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_service.dart b/lib/core/backup_service.dart new file mode 100644 index 000000000..5851505e6 --- /dev/null +++ b/lib/core/backup_service.dart @@ -0,0 +1,301 @@ +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'; +import 'package:cake_wallet/.secrets.g.dart' as secrets; + +class BackupService { + 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 Box _walletInfoSource; + final KeyService _keyService; + + 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: + 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 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); + 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 _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); + 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 { + const defaultSettingsMigrationVersionKey = + 'current_default_settings_migration_version'; + + 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.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), + 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)); + 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 2b337b59b..2f12d0bc3 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_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'; @@ -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'; @@ -21,6 +24,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'; @@ -34,6 +39,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'; @@ -48,13 +54,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/transaction_details_view_model.dart'; import 'package:cake_wallet/view_model/trade_details_view_model.dart'; @@ -96,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