From c09a41b090aa19202321f9d3f269e63301c404ec Mon Sep 17 00:00:00 2001 From: M Date: Wed, 13 Jan 2021 18:43:34 +0200 Subject: [PATCH 1/2] Backup. --- ios/Podfile.lock | 50 ++++ lib/core/backup.dart | 244 ++++++++++++++++++ lib/di.dart | 59 ++++- lib/entities/secret_store_key.dart | 16 +- lib/main.dart | 2 + lib/router.dart | 28 +- lib/routes.dart | 3 + lib/src/screens/backup/backup_page.dart | 93 +++++++ .../backup/edit_backup_password_page.dart | 73 ++++++ .../restore/restore_from_backup_page.dart | 88 +++++++ .../screens/restore/restore_options_page.dart | 4 +- lib/src/widgets/trail_button.dart | 2 +- lib/store/secret_store.dart | 28 ++ lib/view_model/backup_view_model.dart | 68 +++++ .../edit_backup_password_view_model.dart | 44 ++++ .../restore_from_backup_view_model.dart | 29 +++ pubspec.lock | 16 +- pubspec.yaml | 3 + 18 files changed, 826 insertions(+), 24 deletions(-) create mode 100644 lib/core/backup.dart create mode 100644 lib/src/screens/backup/backup_page.dart create mode 100644 lib/src/screens/backup/edit_backup_password_page.dart create mode 100644 lib/src/screens/restore/restore_from_backup_page.dart create mode 100644 lib/store/secret_store.dart create mode 100644 lib/view_model/backup_view_model.dart create mode 100644 lib/view_model/edit_backup_password_view_model.dart create mode 100644 lib/view_model/restore_from_backup_view_model.dart diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 6bf6752d0..c66fa1ecb 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -26,8 +26,42 @@ PODS: - Flutter - devicelocale (0.0.1): - Flutter + - DKImagePickerController/Core (4.3.2): + - DKImagePickerController/ImageDataManager + - DKImagePickerController/Resource + - DKImagePickerController/ImageDataManager (4.3.2) + - DKImagePickerController/PhotoGallery (4.3.2): + - DKImagePickerController/Core + - DKPhotoGallery + - DKImagePickerController/Resource (4.3.2) + - DKPhotoGallery (0.0.17): + - DKPhotoGallery/Core (= 0.0.17) + - DKPhotoGallery/Model (= 0.0.17) + - DKPhotoGallery/Preview (= 0.0.17) + - DKPhotoGallery/Resource (= 0.0.17) + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Core (0.0.17): + - DKPhotoGallery/Model + - DKPhotoGallery/Preview + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Model (0.0.17): + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Preview (0.0.17): + - DKPhotoGallery/Model + - DKPhotoGallery/Resource + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Resource (0.0.17): + - SDWebImage + - SwiftyGif - esys_flutter_share (0.0.1): - Flutter + - file_picker (0.0.1): + - DKImagePickerController/PhotoGallery + - Flutter - Flutter (1.0.0) - flutter_secure_storage (3.3.1): - Flutter @@ -39,11 +73,15 @@ PODS: - path_provider (0.0.1): - Flutter - Reachability (3.2) + - SDWebImage (5.9.1): + - SDWebImage/Core (= 5.9.1) + - SDWebImage/Core (5.9.1) - share (0.0.1): - Flutter - shared_preferences (0.0.1): - Flutter - SwiftProtobuf (1.12.0) + - SwiftyGif (5.3.0) - url_launcher (0.0.1): - Flutter @@ -54,6 +92,7 @@ DEPENDENCIES: - cw_monero (from `.symlinks/plugins/cw_monero/ios`) - devicelocale (from `.symlinks/plugins/devicelocale/ios`) - esys_flutter_share (from `.symlinks/plugins/esys_flutter_share/ios`) + - file_picker (from `.symlinks/plugins/file_picker/ios`) - Flutter (from `Flutter`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - local_auth (from `.symlinks/plugins/local_auth/ios`) @@ -66,9 +105,13 @@ DEPENDENCIES: SPEC REPOS: https://github.com/CocoaPods/Specs.git: - CryptoSwift + - DKImagePickerController + - DKPhotoGallery - MTBBarcodeScanner - Reachability + - SDWebImage - SwiftProtobuf + - SwiftyGif EXTERNAL SOURCES: barcode_scan: @@ -81,6 +124,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/devicelocale/ios" esys_flutter_share: :path: ".symlinks/plugins/esys_flutter_share/ios" + file_picker: + :path: ".symlinks/plugins/file_picker/ios" Flutter: :path: Flutter flutter_secure_storage: @@ -104,7 +149,10 @@ SPEC CHECKSUMS: CryptoSwift: 093499be1a94b0cae36e6c26b70870668cb56060 cw_monero: 2e1f79929880cc2293b5bc1b25e28152e4d84649 devicelocale: feebbe5e7a30adb8c4f83185de1b50ff19b44f00 + DKImagePickerController: b5eb7f7a388e4643264105d648d01f727110fc3d + DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 esys_flutter_share: 403498dab005b36ce1f8d7aff377e81f0621b0b4 + file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1 Flutter: 0e3d915762c693b495b44d77113d4970485de6ec flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec local_auth: 25938960984c3a7f6e3253e3f8d962fdd16852bd @@ -112,9 +160,11 @@ SPEC CHECKSUMS: package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62 path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96 + SDWebImage: a990c053fff71e388a10f3357edb0be17929c9c5 share: 0b2c3e82132f5888bccca3351c504d0003b3b410 shared_preferences: af6bfa751691cdc24be3045c43ec037377ada40d SwiftProtobuf: 4ef85479c18ca85b5482b343df9c319c62bda699 + SwiftyGif: e466e86c660d343357ab944a819a101c4127cb40 url_launcher: 6fef411d543ceb26efce54b05a0a40bfd74cbbef PODFILE CHECKSUM: ba3d2157523e2f4dc333b987efdac6635da8125d diff --git a/lib/core/backup.dart b/lib/core/backup.dart new file mode 100644 index 000000000..d4626fc02 --- /dev/null +++ b/lib/core/backup.dart @@ -0,0 +1,244 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; +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/auth_service.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:cake_wallet/entities/wallet_info.dart'; + +class BackupService { + BackupService(this._flutterSecureStorage, this._authService, + this._walletInfoSource, this._keyService, this._sharedPreferences) + : _cipher = chacha20Poly1305Aead; + + final Cipher _cipher; + final FlutterSecureStorage _flutterSecureStorage; + final SharedPreferences _sharedPreferences; + final AuthService _authService; + final Box _walletInfoSource; + final KeyService _keyService; + + Future importBackup(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 data = file.content as List; + File('${appDir.path}/' + filename) + ..createSync(recursive: true) + ..writeAsBytesSync(data); + } else { + Directory('${appDir.path}/' + filename)..create(recursive: true); + } + + print(filename); + }); + + await importKeychainDump(password, nonce: nonce); + await importPreferencesDump(); + } + + 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; + print('data $data'); + + await _sharedPreferences.setString(PreferencesKey.currentWalletName, + data[PreferencesKey.currentWalletName] as String); + 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, + data[PreferencesKey.currentWalletType] as int); + await _sharedPreferences.setString(PreferencesKey.currentFiatCurrencyKey, + data[PreferencesKey.currentFiatCurrencyKey] as String); + await _sharedPreferences.setBool( + PreferencesKey.shouldSaveRecipientAddressKey, + data[PreferencesKey.shouldSaveRecipientAddressKey] as bool); + await _sharedPreferences.setInt( + PreferencesKey.currentTransactionPriorityKey, + data[PreferencesKey.currentTransactionPriorityKey] as int); + await _sharedPreferences.setBool( + PreferencesKey.allowBiometricalAuthenticationKey, + data[PreferencesKey.allowBiometricalAuthenticationKey] as bool); + await _sharedPreferences.setInt( + PreferencesKey.currentBitcoinElectrumSererIdKey, + data[PreferencesKey.currentBitcoinElectrumSererIdKey] as int); + await _sharedPreferences.setInt(PreferencesKey.currentLanguageCode, + data[PreferencesKey.currentLanguageCode] as int); + await _sharedPreferences.setInt(PreferencesKey.displayActionListModeKey, + data[PreferencesKey.displayActionListModeKey] as int); + await _sharedPreferences.setInt( + 'current_theme', data['current_theme'] as int); + + await preferencesFile.delete(); + } + + Future importKeychainDump(String password, + {@required String nonce}) async { + final appDir = await getApplicationDocumentsDirectory(); + final keychainDumpFile = File('${appDir.path}/~_keychain_dump'); + final decryptedKeychainDumpFileData = + await _decrypt(keychainDumpFile.readAsBytesSync(), 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); + + 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 exportBackup(String password, + {@required String nonce}) 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); + zipEncoder.addFile(preferencesDumpFile, '~_preferences_dump'); + zipEncoder.addFile(keychainDumpFile, '~_keychain_dump'); + zipEncoder.close(); + + final content = File(archivePath).readAsBytesSync(); + tmpDir.deleteSync(recursive: true); + + return await _encrypt(content, password, nonce); + } + + Future exportKeychainDump(String password, + {@required String nonce}) 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 data = + utf8.encode(json.encode({'pin': decodedPin, 'wallets': wallets})); + final encrypted = await _encrypt(Uint8List.fromList(data), password, nonce); + + return encrypted; + } + + Future exportPreferencesJSON() async { + 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.currentDarkTheme: + _sharedPreferences.getBool(PreferencesKey.currentDarkTheme), + PreferencesKey.currentPinLength: + _sharedPreferences.getInt(PreferencesKey.currentPinLength), + PreferencesKey.currentTransactionPriorityKey: _sharedPreferences + .getInt(PreferencesKey.currentTransactionPriorityKey), + PreferencesKey.allowBiometricalAuthenticationKey: _sharedPreferences + .getBool(PreferencesKey.allowBiometricalAuthenticationKey), + PreferencesKey.currentBitcoinElectrumSererIdKey: _sharedPreferences + .getInt(PreferencesKey.currentBitcoinElectrumSererIdKey), + PreferencesKey.currentLanguageCode: + _sharedPreferences.getString(PreferencesKey.currentLanguageCode), + PreferencesKey.displayActionListModeKey: + _sharedPreferences.getInt(PreferencesKey.displayActionListModeKey), + 'currentTheme': _sharedPreferences.getInt('current_theme') + // FIX-ME: Unnamed constant. + }; + + return json.encode(preferences); + } + + Future _encrypt( + Uint8List data, String secretKeySource, String nonceBase64) async { + final secretKeyHash = await sha256.hash(utf8.encode(secretKeySource)); + final secretKey = SecretKey(secretKeyHash.bytes); + final nonce = Nonce(base64.decode(nonceBase64)); + return await _cipher.encrypt(data, secretKey: secretKey, nonce: nonce); + } + + Future _decrypt( + Uint8List data, String secretKeySource, String nonceBase64) async { + 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); + } +} diff --git a/lib/di.dart b/lib/di.dart index fd73da8fc..f6cfd0aa6 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/bitcoin/bitcoin_wallet_service.dart'; +import 'package:cake_wallet/core/backup.dart'; import 'package:cake_wallet/core/wallet_service.dart'; import 'package:cake_wallet/entities/biometric_auth.dart'; import 'package:cake_wallet/entities/contact_record.dart'; @@ -10,6 +11,8 @@ import 'package:cake_wallet/entities/contact.dart'; import 'package:cake_wallet/entities/node.dart'; import 'package:cake_wallet/exchange/trade.dart'; import 'package:cake_wallet/reactions/on_authentication_state_change.dart'; +import 'package:cake_wallet/src/screens/backup/backup_page.dart'; +import 'package:cake_wallet/src/screens/backup/edit_backup_password_page.dart'; import 'package:cake_wallet/src/screens/contact/contact_list_page.dart'; import 'package:cake_wallet/src/screens/contact/contact_page.dart'; @@ -20,6 +23,8 @@ import 'package:cake_wallet/src/screens/nodes/node_create_or_edit_page.dart'; import 'package:cake_wallet/src/screens/nodes/nodes_list_page.dart'; import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart'; import 'package:cake_wallet/src/screens/rescan/rescan_page.dart'; +import 'package:cake_wallet/src/screens/restore/restore_from_backup_page.dart'; +import 'package:cake_wallet/src/screens/restore/restore_options_page.dart'; import 'package:cake_wallet/src/screens/restore/wallet_restore_page.dart'; import 'package:cake_wallet/src/screens/seed/pre_seed_page.dart'; import 'package:cake_wallet/src/screens/seed/wallet_seed_page.dart'; @@ -32,6 +37,7 @@ import 'package:cake_wallet/src/screens/wallet_keys/wallet_keys_page.dart'; import 'package:cake_wallet/src/screens/exchange/exchange_page.dart'; import 'package:cake_wallet/src/screens/exchange/exchange_template_page.dart'; import 'package:cake_wallet/store/node_list_store.dart'; +import 'package:cake_wallet/store/secret_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/core/key_service.dart'; @@ -46,13 +52,16 @@ import 'package:cake_wallet/src/screens/send/send_page.dart'; import 'package:cake_wallet/src/screens/subaddress/address_edit_or_create_page.dart'; import 'package:cake_wallet/src/screens/wallet_list/wallet_list_page.dart'; import 'package:cake_wallet/store/wallet_list_store.dart'; +import 'package:cake_wallet/view_model/backup_view_model.dart'; import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart'; import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart'; +import 'package:cake_wallet/view_model/edit_backup_password_view_model.dart'; import 'package:cake_wallet/view_model/exchange/exchange_trade_view_model.dart'; import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart'; import 'package:cake_wallet/view_model/node_list/node_list_view_model.dart'; import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart'; import 'package:cake_wallet/view_model/rescan_view_model.dart'; +import 'package:cake_wallet/view_model/restore_from_backup_view_model.dart'; import 'package:cake_wallet/view_model/setup_pin_code_view_model.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart'; import 'package:cake_wallet/view_model/auth_view_model.dart'; @@ -127,6 +136,11 @@ Future setup( getIt.registerSingleton( ExchangeTemplateStore(templateSource: exchangeTemplates)); + final secretStore = + await SecretStoreBase.load(getIt.get()); + + getIt.registerSingleton(secretStore); + getIt.registerFactory( () => KeyService(getIt.get())); @@ -291,17 +305,16 @@ Future setup( getIt.get()));*/ getIt.registerFactoryParam( - (AccountListItem account, _) => - MoneroAccountEditOrCreateViewModel(( - getIt.get().wallet as MoneroWallet).accountList, - accountListItem: account)); + AccountListItem, void>( + (AccountListItem account, _) => MoneroAccountEditOrCreateViewModel( + (getIt.get().wallet as MoneroWallet).accountList, + accountListItem: account)); - getIt.registerFactoryParam((AccountListItem account, _) => - MoneroAccountEditOrCreatePage( + getIt.registerFactoryParam( + (AccountListItem account, _) => MoneroAccountEditOrCreatePage( moneroAccountCreationViewModel: - getIt.get(param1: account))); + getIt.get(param1: account))); getIt.registerFactory(() { final appStore = getIt.get(); @@ -422,4 +435,32 @@ Future setup( transactionDescriptionBox)); getIt.registerFactory(() => PreSeedPage()); + + getIt.registerFactory(() => BackupService( + getIt.get(), + getIt.get(), + walletInfoSource, + getIt.get(), + getIt.get())); + + getIt.registerFactory(() => BackupViewModel(getIt.get(), + getIt.get(), getIt.get())); + + getIt.registerFactory(() => BackupPage(getIt.get())); + + getIt.registerFactory(() => EditBackupPasswordViewModel( + getIt.get(), getIt.get()) + ..init()); + + getIt.registerFactory( + () => EditBackupPasswordPage(getIt.get())); + + getIt.registerFactoryParam( + (WalletType type, _) => RestoreOptionsPage(type: type)); + + getIt.registerFactory( + () => RestoreFromBackupViewModel(getIt.get())); + + getIt.registerFactory( + () => RestoreFromBackupPage(getIt.get())); } diff --git a/lib/entities/secret_store_key.dart b/lib/entities/secret_store_key.dart index 3a25a7132..92eefa221 100644 --- a/lib/entities/secret_store_key.dart +++ b/lib/entities/secret_store_key.dart @@ -1,9 +1,13 @@ -enum SecretStoreKey { moneroWalletPassword, pinCodePassword } +enum SecretStoreKey { moneroWalletPassword, pinCodePassword, backupPassword } const moneroWalletPassword = "MONERO_WALLET_PASSWORD"; const pinCodePassword = "PIN_CODE_PASSWORD"; +const backupPassword = "BACKUP_CODE_PASSWORD"; -String generateStoreKeyFor({SecretStoreKey key, String walletName = "",}) { +String generateStoreKeyFor({ + SecretStoreKey key, + String walletName = "", +}) { var _key = ""; switch (key) { @@ -19,9 +23,15 @@ String generateStoreKeyFor({SecretStoreKey key, String walletName = "",}) { } break; + case SecretStoreKey.backupPassword: + { + _key = backupPassword; + } + break; + default: {} } return _key; -} \ No newline at end of file +} diff --git a/lib/main.dart b/lib/main.dart index 9cd55e054..7813bd375 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,5 @@ +import 'package:cake_wallet/core/backup.dart'; +import 'package:cake_wallet/src/screens/backup/backup_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:hive/hive.dart'; diff --git a/lib/router.dart b/lib/router.dart index ef38c97f8..9ce482295 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -1,6 +1,9 @@ import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/transaction_description.dart'; +import 'package:cake_wallet/src/screens/backup/backup_page.dart'; +import 'package:cake_wallet/src/screens/backup/edit_backup_password_page.dart'; import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart'; +import 'package:cake_wallet/src/screens/restore/restore_from_backup_page.dart'; import 'package:cake_wallet/src/screens/restore/wallet_restore_page.dart'; import 'package:cake_wallet/src/screens/seed/pre_seed_page.dart'; import 'package:cake_wallet/store/settings_store.dart'; @@ -65,15 +68,13 @@ Route createRoute(RouteSettings settings) { builder: (_) => getIt.get(param1: (PinCodeState context, dynamic _) async { try { - context.changeProcessText( - S.current.creating_new_wallet); + context.changeProcessText(S.current.creating_new_wallet); final newWalletVM = getIt.get(param1: WalletType.monero); await newWalletVM.create( options: 'English'); // FIXME: Unnamed constant context.hideProgressText(); - await Navigator.of(context.context) - .pushNamed(Routes.preSeed); + await Navigator.of(context.context).pushNamed(Routes.preSeed); } catch (e) { context.changeProcessText('Error: ${e.toString()}'); } @@ -115,7 +116,7 @@ Route createRoute(RouteSettings settings) { case Routes.restoreOptions: final type = settings.arguments as WalletType; return CupertinoPageRoute( - builder: (_) => RestoreOptionsPage(type: type)); + builder: (_) => getIt.get(param1: type)); case Routes.restoreWalletOptions: final type = WalletType.monero; //settings.arguments as WalletType; @@ -264,7 +265,7 @@ Route createRoute(RouteSettings settings) { case Routes.accountCreation: return CupertinoPageRoute( builder: (_) => getIt.get( - param1: settings.arguments as AccountListItem)); + param1: settings.arguments as AccountListItem)); case Routes.addressBook: return MaterialPageRoute( @@ -326,8 +327,19 @@ Route createRoute(RouteSettings settings) { builder: (_) => getIt.get()); case Routes.preSeed: - return MaterialPageRoute( - builder: (_) => getIt.get()); + return MaterialPageRoute(builder: (_) => getIt.get()); + + case Routes.backup: + return CupertinoPageRoute( + fullscreenDialog: true, builder: (_) => getIt.get()); + + case Routes.editBackupPassword: + return CupertinoPageRoute( + builder: (_) => getIt.get()); + + case Routes.restoreFromBackup: + return CupertinoPageRoute( + builder: (_) => getIt.get()); default: return MaterialPageRoute( diff --git a/lib/routes.dart b/lib/routes.dart index a9c9a5b34..a7cb8cab9 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -48,4 +48,7 @@ class Routes { static const restoreWalletType = '/restore_wallet_type'; static const restoreWallet = '/restore_wallet'; static const preSeed = '/pre_seed'; + static const backup = '/backup'; + static const editBackupPassword = '/edit_backup_passowrd'; + static const restoreFromBackup = '/restore_from_backup'; } \ No newline at end of file diff --git a/lib/src/screens/backup/backup_page.dart b/lib/src/screens/backup/backup_page.dart new file mode 100644 index 000000000..fdcde49e0 --- /dev/null +++ b/lib/src/screens/backup/backup_page.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:esys_flutter_share/esys_flutter_share.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/src/widgets/trail_button.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/backup_view_model.dart'; +import 'package:cake_wallet/core/execution_state.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; + +class BackupPage extends BasePage { + BackupPage(this.backupViewModelBase); + + final BackupViewModelBase backupViewModelBase; + + @override + String get title => 'Backup'; + + @override + Widget trailing(BuildContext context) => TrailButton( + caption: S.of(context).edit, + onPressed: () => + Navigator.of(context).pushNamed(Routes.editBackupPassword)); + + @override + Widget body(BuildContext context) { + return Stack( + fit: StackFit.expand, + children: [ + Center( + child: Container( + padding: EdgeInsets.only(left: 20, right: 20), + height: 225, + child: Column(children: [ + Text( + 'Backup password:', + style: TextStyle(fontSize: 30), + ), + Padding( + padding: EdgeInsets.only(top: 20, bottom: 10), + child: Observer( + builder: (_) => Text( + backupViewModelBase.backupPassword, + style: TextStyle(fontSize: 26), + ))), + Padding( + padding: EdgeInsets.all(20), + child: Text( + 'Please write down your Backup Password. Backup Password uses for import of backup files.', + style: TextStyle(fontSize: 14, color: Colors.grey), + textAlign: TextAlign.center, + )) + ]))), + Positioned( + child: Observer( + builder: (_) => LoadingPrimaryButton( + isLoading: backupViewModelBase.state is IsExecutingState, + onPressed: () => onExportBackup(context), + text: 'Export backup', + color: Theme.of(context).accentTextTheme.body2.color, + textColor: Colors.white)), + bottom: 30, + left: 20, + right: 20, + ) + ], + ); + } + + void onExportBackup(BuildContext context) { + showPopUp( + context: context, + builder: (dialogContext) { + return AlertWithTwoActions( + alertTitle: 'Export backup', + alertContent: + 'Please be sure that you have saved your Backup Password.You will be no available to import backup files without Backup Password.\n\nHave you written it down?', + rightButtonText: S.of(context).seed_alert_yes, + leftButtonText: S.of(context).seed_alert_back, + actionRightButton: () async { + Navigator.of(dialogContext).pop(); + final backup = await backupViewModelBase.exportBackup(); + await Share.file( + 'Backup file', backup.name, backup.content, 'text'); + }, + actionLeftButton: () => Navigator.of(dialogContext).pop()); + }); + } +} diff --git a/lib/src/screens/backup/edit_backup_password_page.dart b/lib/src/screens/backup/edit_backup_password_page.dart new file mode 100644 index 000000000..d0f4c8fd1 --- /dev/null +++ b/lib/src/screens/backup/edit_backup_password_page.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/view_model/edit_backup_password_view_model.dart'; + +class EditBackupPasswordPage extends BasePage { + EditBackupPasswordPage(this.editBackupPasswordViewModel) + : textEditingController = TextEditingController() { + textEditingController.text = editBackupPasswordViewModel.backupPassword; + textEditingController.addListener(() => editBackupPasswordViewModel + .backupPassword = textEditingController.text); + } + + final EditBackupPasswordViewModel editBackupPasswordViewModel; + final TextEditingController textEditingController; + + @override + String get title => 'Edit Backup Password'; + + @override + Widget body(BuildContext context) { + return Padding( + padding: EdgeInsets.only(left: 20, right: 20), + child: Stack( + fit: StackFit.expand, + children: [ + Center( + child: Observer( + builder: (_) => TextFormField( + autofocus: true, + enableSuggestions: false, + autocorrect: false, + keyboardType: TextInputType.visiblePassword, + controller: textEditingController, + style: TextStyle(fontSize: 26, color: Colors.black)))), + Positioned( + child: Observer( + builder: (_) => PrimaryButton( + onPressed: () => onSave(context), + text: 'Save', + color: Theme.of(context).accentTextTheme.body2.color, + textColor: Colors.white, + isDisabled: !editBackupPasswordViewModel.canSave)), + bottom: 30, + left: 0, + right: 0) + ], + )); + } + + void onSave(BuildContext context) { + showPopUp( + context: context, + builder: (dialogContext) { + return AlertWithTwoActions( + alertTitle: 'Save backup password', + alertContent: + 'Your previous backup files will be not available to import with new backup password. New backup password will be used only for new backup files. Are you sure that you want to change backup password ?', + rightButtonText: S.of(context).ok, + leftButtonText: S.of(context).cancel, + actionRightButton: () async { + await editBackupPasswordViewModel.save(); + Navigator.of(dialogContext)..pop()..pop(); + }, + actionLeftButton: () => Navigator.of(dialogContext).pop()); + }); + } +} diff --git a/lib/src/screens/restore/restore_from_backup_page.dart b/lib/src/screens/restore/restore_from_backup_page.dart new file mode 100644 index 000000000..a18d7bb79 --- /dev/null +++ b/lib/src/screens/restore/restore_from_backup_page.dart @@ -0,0 +1,88 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/framework.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/view_model/restore_from_backup_view_model.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:file_picker/file_picker.dart'; + +class RestoreFromBackupPage extends BasePage { + RestoreFromBackupPage(this.restoreFromBackupViewModel) + : textEditingController = TextEditingController(); + + final RestoreFromBackupViewModel restoreFromBackupViewModel; + final TextEditingController textEditingController; + + @override + String get title => 'Restore from backup'; + + @override + Widget body(BuildContext context) { + return Container( + padding: EdgeInsets.only(bottom: 30, left: 25, right: 25), + child: Column(children: [ + Expanded( + child: Container( + child: Center( + child: TextFormField( + obscureText: true, + enableSuggestions: false, + autocorrect: false, + decoration: InputDecoration( + hintText: 'Enter backup password here'), + keyboardType: TextInputType.visiblePassword, + controller: textEditingController, + style: TextStyle(fontSize: 26, color: Colors.black))), + ), + ), + Container( + child: Row(children: [ + Expanded( + child: PrimaryButton( + onPressed: () => presentFilePicker(), + text: 'Select backup file', + color: Colors.grey, + textColor: Colors.white)), + SizedBox(width: 20), + Expanded( + child: PrimaryButton( + onPressed: () => onImportHandler(context), + text: 'Import', + color: Theme.of(context).accentTextTheme.body2.color, + textColor: Colors.white)) + ])), + ])); + } + + Future presentFilePicker() async { + final result = await FilePicker.platform.pickFiles(); + + if (result?.files?.isEmpty ?? true) { + return; + } + + restoreFromBackupViewModel.filePath = result.files.first.path; + } + + Future onImportHandler(BuildContext context) async { + if (textEditingController.text.isEmpty || + (restoreFromBackupViewModel.filePath.isEmpty ?? true)) { + await showPopUp( + context: context, + builder: (_) { + return AlertWithOneAction( + alertTitle: S.current.error, + alertContent: + 'Please select backup file and enter backup password.', + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); + + return; + } + + await restoreFromBackupViewModel.import(textEditingController.text); + } +} diff --git a/lib/src/screens/restore/restore_options_page.dart b/lib/src/screens/restore/restore_options_page.dart index bce520170..6ac070d7d 100644 --- a/lib/src/screens/restore/restore_options_page.dart +++ b/lib/src/screens/restore/restore_options_page.dart @@ -1,6 +1,5 @@ import 'package:cake_wallet/entities/wallet_type.dart'; import 'package:flutter/material.dart'; -import 'package:cake_wallet/palette.dart'; import 'package:cake_wallet/routes.dart'; import 'package:flutter/cupertino.dart'; import 'package:cake_wallet/src/screens/restore/widgets/restore_button.dart'; @@ -39,7 +38,8 @@ class RestoreOptionsPage extends BasePage { Padding( padding: EdgeInsets.only(top: 24), child: RestoreButton( - onPressed: () {}, + onPressed: () => Navigator.pushNamed( + context, Routes.restoreFromBackup), image: imageBackup, title: S.of(context).restore_title_from_backup, description: S.of(context).restore_description_from_backup diff --git a/lib/src/widgets/trail_button.dart b/lib/src/widgets/trail_button.dart index a7c4caecb..64a11be1c 100644 --- a/lib/src/widgets/trail_button.dart +++ b/lib/src/widgets/trail_button.dart @@ -21,7 +21,7 @@ class TrailButton extends StatelessWidget { child: Text( caption, style: TextStyle( - color: Theme.of(context).textTheme.subhead.decorationColor, + color: Theme.of(context).textTheme.display1.color, fontWeight: FontWeight.w500, fontSize: 14), ), diff --git a/lib/store/secret_store.dart b/lib/store/secret_store.dart new file mode 100644 index 000000000..a46f3e1fe --- /dev/null +++ b/lib/store/secret_store.dart @@ -0,0 +1,28 @@ +import 'package:cake_wallet/entities/secret_store_key.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:mobx/mobx.dart'; + +part 'secret_store.g.dart'; + +class SecretStore = SecretStoreBase with _$SecretStore; + +abstract class SecretStoreBase with Store { + static Future load(FlutterSecureStorage storage) async { + final secretStore = SecretStore(); + final backupPasswordKey = generateStoreKeyFor(key: SecretStoreKey.backupPassword); + final backupPassword = await storage.read(key: backupPasswordKey); + secretStore.write(key: backupPasswordKey, value: backupPassword); + + return secretStore; + } + + SecretStoreBase() : values = ObservableMap(); + + ObservableMap values; + + String read(String key) => values[key] as String; + + String write({@required String key, @required String value}) => + values[key] = value; +} diff --git a/lib/view_model/backup_view_model.dart b/lib/view_model/backup_view_model.dart new file mode 100644 index 000000000..1f4b8f5d8 --- /dev/null +++ b/lib/view_model/backup_view_model.dart @@ -0,0 +1,68 @@ +import 'package:cake_wallet/core/backup.dart'; +import 'package:cake_wallet/core/execution_state.dart'; +import 'package:cake_wallet/entities/secret_store_key.dart'; +import 'package:cake_wallet/store/secret_store.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:mobx/mobx.dart'; + +part 'backup_view_model.g.dart'; + +class BackupExportFile { + BackupExportFile(this.content, {@required this.name}); + + final String name; + final List content; +} + +class BackupViewModel = BackupViewModelBase with _$BackupViewModel; + +abstract class BackupViewModelBase with Store { + BackupViewModelBase(this.secureStorage, this.secretStore, this.backupService) + : isBackupPasswordVisible = false { + state = InitialExecutionState(); + final key = generateStoreKeyFor(key: SecretStoreKey.backupPassword); + secretStore.values.observe((change) { + if (change.key == key) { + backupPassword = secretStore.read(key); + } + }, fireImmediately: true); + } + + final FlutterSecureStorage secureStorage; + final SecretStore secretStore; + final BackupService backupService; + + @observable + ExecutionState state; + + @observable + bool isBackupPasswordVisible; + + @observable + String backupPassword; + + @action + Future init() async { + final key = generateStoreKeyFor(key: SecretStoreKey.backupPassword); + backupPassword = await secureStorage.read(key: key); + } + + @action + Future exportBackup() async { + try { + state = IsExecutingState(); + final backupContent = await backupService.exportBackup('', nonce: ''); + state = ExecutedSuccessfullyState(); + + return BackupExportFile(backupContent.toList(), + name: 'backup_${DateTime.now().toString()}.zip'); + } catch (e) { + print(e.toString()); + state = FailureState(e.toString()); + } + } + + @action + void showMasterPassword() => isBackupPasswordVisible = true; +} diff --git a/lib/view_model/edit_backup_password_view_model.dart b/lib/view_model/edit_backup_password_view_model.dart new file mode 100644 index 000000000..d4f66da74 --- /dev/null +++ b/lib/view_model/edit_backup_password_view_model.dart @@ -0,0 +1,44 @@ +import 'package:mobx/mobx.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:cake_wallet/entities/secret_store_key.dart'; +import 'package:cake_wallet/store/secret_store.dart'; + +part 'edit_backup_password_view_model.g.dart'; + +class EditBackupPasswordViewModel = EditBackupPasswordViewModelBase + with _$EditBackupPasswordViewModel; + +abstract class EditBackupPasswordViewModelBase with Store { + EditBackupPasswordViewModelBase(this.secureStorage, this.secretStore) { + final key = generateStoreKeyFor(key: SecretStoreKey.backupPassword); + backupPassword = secretStore.read(key); + } + + final FlutterSecureStorage secureStorage; + final SecretStore secretStore; + + @observable + String backupPassword; + + @computed + bool get canSave { + return !(_originalPassword == backupPassword); + } + + String _originalPassword; + + @action + Future init() async { + final key = generateStoreKeyFor(key: SecretStoreKey.backupPassword); + final password = await secureStorage.read(key: key); + _originalPassword = password; + backupPassword = password; + } + + @action + Future save() async { + final key = generateStoreKeyFor(key: SecretStoreKey.backupPassword); + await secureStorage.write(key: key, value: backupPassword); + secretStore.write(key: key, value: backupPassword); + } +} diff --git a/lib/view_model/restore_from_backup_view_model.dart b/lib/view_model/restore_from_backup_view_model.dart new file mode 100644 index 000000000..c103d9cfa --- /dev/null +++ b/lib/view_model/restore_from_backup_view_model.dart @@ -0,0 +1,29 @@ +import 'dart:io'; + +import 'package:cake_wallet/core/backup.dart'; +import 'package:mobx/mobx.dart'; + +part 'restore_from_backup_view_model.g.dart'; + +class RestoreFromBackupViewModel = RestoreFromBackupViewModelBase with _$RestoreFromBackupViewModel; + +abstract class RestoreFromBackupViewModelBase with Store { + RestoreFromBackupViewModelBase(this.backupService); + + @observable + String filePath; + + final BackupService backupService; + + Future import(String password) async { + if (filePath?.isEmpty ?? true) { + // FIXME: throw exception; + return; + } + + final file = File(filePath); + final data = await file.readAsBytes(); + + await backupService.importBackup(data, password, nonce: null); + } +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index f254541af..bd6723765 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -16,7 +16,7 @@ packages: source: hosted version: "0.40.6" archive: - dependency: transitive + dependency: "direct main" description: name: archive url: "https://pub.dartlang.org" @@ -253,6 +253,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.5" + cryptography: + dependency: "direct main" + description: + name: cryptography + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.1" cupertino_icons: dependency: "direct main" description: @@ -344,6 +351,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "5.2.1" + file_picker: + dependency: "direct main" + description: + name: file_picker + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.4" fixnum: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c8b1f53c4..673566b44 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -67,6 +67,9 @@ dependencies: connectivity: ^0.4.9+2 keyboard_actions: ^3.3.0 flushbar: ^1.10.4 + archive: ^2.0.13 + cryptography: ^1.4.0 + file_picker: ^2.1.4 dev_dependencies: flutter_test: From 47ceac2dd6101a1071c2afb5486f03747eb1351e Mon Sep 17 00:00:00 2001 From: M Date: Fri, 15 Jan 2021 19:41:30 +0200 Subject: [PATCH 2/2] Backup stuff. --- lib/core/{backup.dart => backup_service.dart} | 275 +++++++++++------- lib/di.dart | 79 +++-- lib/entities/contact.dart | 3 +- lib/entities/default_settings_migration.dart | 29 +- lib/entities/node.dart | 3 +- lib/entities/preferences_key.dart | 1 + lib/entities/template.dart | 3 +- lib/entities/transaction_description.dart | 3 +- lib/entities/wallet_info.dart | 3 +- lib/entities/wallet_type.dart | 3 +- lib/exchange/exchange_template.dart | 3 +- lib/exchange/trade.dart | 3 +- lib/main.dart | 99 ++++--- lib/reactions/bootstrap.dart | 14 +- lib/router.dart | 3 +- lib/src/screens/backup/backup_page.dart | 22 +- .../restore/restore_from_backup_page.dart | 1 + .../screens/restore/restore_options_page.dart | 59 ++-- lib/src/screens/welcome/welcome_page.dart | 2 +- lib/src/widgets/trail_button.dart | 2 +- lib/store/settings_store.dart | 33 ++- lib/view_model/backup_view_model.dart | 5 +- .../restore_from_backup_view_model.dart | 51 +++- .../settings/settings_view_model.dart | 13 + 24 files changed, 459 insertions(+), 253 deletions(-) rename lib/core/{backup.dart => backup_service.dart} (72%) diff --git a/lib/core/backup.dart b/lib/core/backup_service.dart similarity index 72% rename from lib/core/backup.dart rename to lib/core/backup_service.dart index 760c7bbc0..5851505e6 100644 --- a/lib/core/backup.dart +++ b/lib/core/backup_service.dart @@ -14,128 +14,58 @@ 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:cake_wallet/entities/wallet_info.dart'; +import 'package:cake_wallet/.secrets.g.dart' as secrets; class BackupService { - BackupService(this._flutterSecureStorage, this._authService, - this._walletInfoSource, this._keyService, this._sharedPreferences) + BackupService(this._flutterSecureStorage, this._walletInfoSource, + this._keyService, this._sharedPreferences) : _cipher = chacha20Poly1305Aead; + static const currentVersion = _v1; + + static const _v1 = 1; + final Cipher _cipher; final FlutterSecureStorage _flutterSecureStorage; final SharedPreferences _sharedPreferences; - final AuthService _authService; final Box _walletInfoSource; final KeyService _keyService; Future importBackup(Uint8List data, String password, - {@required String nonce}) async { - final appDir = await getApplicationDocumentsDirectory(); - final decryptedData = await _decrypt(data, password, nonce); - final zip = ZipDecoder().decodeBytes(decryptedData); + {String nonce = secrets.backupSalt}) async { + final version = getVersion(data); + final backupBytes = data.toList()..removeAt(0); + final backupData = Uint8List.fromList(backupBytes); - zip.files.forEach((file) { - final filename = file.name; - - if (file.isFile) { - final data = file.content as List; - File('${appDir.path}/' + filename) - ..createSync(recursive: true) - ..writeAsBytesSync(data); - } else { - Directory('${appDir.path}/' + filename)..create(recursive: true); - } - - print(filename); - }); - - await importKeychainDump(password, nonce: nonce); - await importPreferencesDump(); - } - - Future importPreferencesDump() async { - final appDir = await getApplicationDocumentsDirectory(); - final preferencesFile = File('${appDir.path}/~_preferences_dump'); - - if (!preferencesFile.existsSync()) { - return; + switch (version) { + case _v1: + await _importBackupV1(backupData, password, nonce: nonce); + break; + default: + break; } - - final data = - json.decode(preferencesFile.readAsStringSync()) as Map; - print('data $data'); - - await _sharedPreferences.setString(PreferencesKey.currentWalletName, - data[PreferencesKey.currentWalletName] as String); - 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, - data[PreferencesKey.currentWalletType] as int); - await _sharedPreferences.setString(PreferencesKey.currentFiatCurrencyKey, - data[PreferencesKey.currentFiatCurrencyKey] as String); - await _sharedPreferences.setBool( - PreferencesKey.shouldSaveRecipientAddressKey, - data[PreferencesKey.shouldSaveRecipientAddressKey] as bool); - await _sharedPreferences.setInt( - PreferencesKey.currentTransactionPriorityKey, - data[PreferencesKey.currentTransactionPriorityKey] as int); - await _sharedPreferences.setBool( - PreferencesKey.allowBiometricalAuthenticationKey, - data[PreferencesKey.allowBiometricalAuthenticationKey] as bool); - await _sharedPreferences.setInt( - PreferencesKey.currentBitcoinElectrumSererIdKey, - data[PreferencesKey.currentBitcoinElectrumSererIdKey] as int); - await _sharedPreferences.setInt(PreferencesKey.currentLanguageCode, - data[PreferencesKey.currentLanguageCode] as int); - await _sharedPreferences.setInt(PreferencesKey.displayActionListModeKey, - data[PreferencesKey.displayActionListModeKey] as int); - await _sharedPreferences.setInt( - 'current_theme', data['current_theme'] as int); - - await preferencesFile.delete(); - } - - Future importKeychainDump(String password, - {@required String nonce}) async { - final appDir = await getApplicationDocumentsDirectory(); - final keychainDumpFile = File('${appDir.path}/~_keychain_dump'); - final decryptedKeychainDumpFileData = - await _decrypt(keychainDumpFile.readAsBytesSync(), 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); - - 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 exportBackup(String password, - {@required String nonce}) async { + {String nonce = secrets.backupSalt, int version = currentVersion}) async { + switch (version) { + case _v1: + return await _exportBackupV1(password, nonce: nonce); + default: + return null; + } + } + + 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 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'); @@ -165,12 +95,120 @@ class BackupService { final content = File(archivePath).readAsBytesSync(); tmpDir.deleteSync(recursive: true); + final encryptedData = await _encrypt(content, password, nonce); - return await _encrypt(content, password, nonce); + return setVersion(encryptedData, currentVersion); } - Future exportKeychainDump(String password, + 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 _importKeychainDump(password, nonce: nonce); + await _importPreferencesDump(); + } + + Future _importPreferencesDump() async { + final appDir = await getApplicationDocumentsDirectory(); + final preferencesFile = File('${appDir.path}/~_preferences_dump'); + const defaultSettingsMigrationVersionKey = PreferencesKey.currentDefaultSettingsMigrationVersion; + + if (!preferencesFile.existsSync()) { + return; + } + + final data = + json.decode(preferencesFile.readAsStringSync()) as Map; + + await _sharedPreferences.setString(PreferencesKey.currentWalletName, + data[PreferencesKey.currentWalletName] as String); + 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, + data[PreferencesKey.currentWalletType] as int); + await _sharedPreferences.setString(PreferencesKey.currentFiatCurrencyKey, + data[PreferencesKey.currentFiatCurrencyKey] as String); + await _sharedPreferences.setBool( + PreferencesKey.shouldSaveRecipientAddressKey, + data[PreferencesKey.shouldSaveRecipientAddressKey] as bool); + await _sharedPreferences.setInt( + PreferencesKey.currentTransactionPriorityKey, + data[PreferencesKey.currentTransactionPriorityKey] as int); + await _sharedPreferences.setBool( + PreferencesKey.allowBiometricalAuthenticationKey, + data[PreferencesKey.allowBiometricalAuthenticationKey] as bool); + await _sharedPreferences.setInt( + PreferencesKey.currentBitcoinElectrumSererIdKey, + data[PreferencesKey.currentBitcoinElectrumSererIdKey] as int); + await _sharedPreferences.setInt(PreferencesKey.currentLanguageCode, + data[PreferencesKey.currentLanguageCode] as int); + await _sharedPreferences.setInt(PreferencesKey.displayActionListModeKey, + data[PreferencesKey.displayActionListModeKey] as int); + await _sharedPreferences.setInt( + 'current_theme', data['current_theme'] as int); + await _sharedPreferences.setInt(defaultSettingsMigrationVersionKey, + data[defaultSettingsMigrationVersionKey] 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); @@ -183,15 +221,25 @@ class BackupService { await _keyService.getWalletPassword(walletName: walletInfo.name) }; })); - - final data = - utf8.encode(json.encode({'pin': decodedPin, 'wallets': wallets})); - final encrypted = await _encrypt(Uint8List.fromList(data), password, nonce); + 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 { + Future _exportPreferencesJSON() async { + const defaultSettingsMigrationVersionKey = + 'current_default_settings_migration_version'; + final preferences = { PreferencesKey.currentWalletName: _sharedPreferences.getString(PreferencesKey.currentWalletName), @@ -219,13 +267,22 @@ class BackupService { _sharedPreferences.getString(PreferencesKey.currentLanguageCode), PreferencesKey.displayActionListModeKey: _sharedPreferences.getInt(PreferencesKey.displayActionListModeKey), - PreferencesKey.currentTheme: _sharedPreferences.getInt(PreferencesKey.currentTheme) - // FIX-ME: Unnamed constant. + PreferencesKey.currentTheme: + _sharedPreferences.getInt(PreferencesKey.currentTheme), + defaultSettingsMigrationVersionKey: + _sharedPreferences.getInt(defaultSettingsMigrationVersionKey) }; 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 sha256.hash(utf8.encode(secretKeySource)); diff --git a/lib/di.dart b/lib/di.dart index eddffc232..2f12d0bc3 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -1,5 +1,5 @@ import 'package:cake_wallet/bitcoin/bitcoin_wallet_service.dart'; -import 'package:cake_wallet/core/backup.dart'; +import 'package:cake_wallet/core/backup_service.dart'; import 'package:cake_wallet/core/wallet_service.dart'; import 'package:cake_wallet/entities/biometric_auth.dart'; import 'package:cake_wallet/entities/contact_record.dart'; @@ -105,6 +105,15 @@ import 'package:cake_wallet/exchange/exchange_template.dart'; final getIt = GetIt.instance; +var _isSetupFinished = false; +Box _walletInfoSource; +Box _nodeSource; +Box _contactSource; +Box _tradesSource; +Box