mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2025-06-28 12:29:51 +00:00
CW-875 BackupServiceV3 (#2064)
* implement v3 backup system - Add new BackupServiceV3 with chunked backup support and checksums - Implement json-based metadata in backup system - Instead of binary file export a .zip file that user can open and see that it is a cake backup (should also prevent 3rd party software from corrupting binary data inside of the file, in case it doesn't checksum will fail, and user will know for sure that backup got corrupted) - Update flutter to 3.27.4 to use archive ^4.x.x (it offers in memory archive operations) * fix wallets not getting restored properly * prevent out of memory errors on files that are way too big during restore * Update lib/view_model/backup_view_model.dart [skip ci] * Update lib/core/backup_service.dart [skip ci] * Update lib/core/backup_service.dart --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
parent
a085eff984
commit
102ab8dbe6
36 changed files with 1193 additions and 807 deletions
|
@ -24,118 +24,27 @@ 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._secureStorage, this._walletInfoSource, this._transactionDescriptionBox,
|
||||
this._keyService, this._sharedPreferences)
|
||||
: _cipher = Cryptography.instance.chacha20Poly1305Aead(),
|
||||
_correctWallets = <WalletInfo>[];
|
||||
class $BackupService {
|
||||
$BackupService(this._secureStorage, this.walletInfoSource, this.transactionDescriptionBox,
|
||||
this.keyService, this.sharedPreferences)
|
||||
: cipher = Cryptography.instance.chacha20Poly1305Aead(),
|
||||
correctWallets = <WalletInfo>[];
|
||||
|
||||
static const currentVersion = _v2;
|
||||
static const currentVersion = _v3;
|
||||
|
||||
static const _v1 = 1;
|
||||
static const _v2 = 2;
|
||||
static const _v3 = 3;
|
||||
|
||||
final Cipher _cipher;
|
||||
final Cipher cipher;
|
||||
final SecureStorage _secureStorage;
|
||||
final SharedPreferences _sharedPreferences;
|
||||
final Box<WalletInfo> _walletInfoSource;
|
||||
final Box<TransactionDescription> _transactionDescriptionBox;
|
||||
final KeyService _keyService;
|
||||
List<WalletInfo> _correctWallets;
|
||||
final SharedPreferences sharedPreferences;
|
||||
final Box<WalletInfo> walletInfoSource;
|
||||
final Box<TransactionDescription> transactionDescriptionBox;
|
||||
final KeyService keyService;
|
||||
List<WalletInfo> correctWallets;
|
||||
|
||||
Future<void> importBackup(Uint8List data, String password,
|
||||
{String nonce = secrets.backupSalt}) async {
|
||||
final version = getVersion(data);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Uint8List> exportBackup(String password,
|
||||
{String nonce = secrets.backupSalt, int version = currentVersion}) async {
|
||||
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<Uint8List> _exportBackupV1(String password, {String nonce = secrets.backupSalt}) async =>
|
||||
throw Exception('Deprecated. Export for backups v1 is deprecated. Please use export v2.');
|
||||
|
||||
Future<Uint8List> _exportBackupV2(String password) async {
|
||||
final zipEncoder = ZipFileEncoder();
|
||||
final appDir = await getAppDir();
|
||||
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 _exportKeychainDumpV2(password);
|
||||
final preferencesDump = await _exportPreferencesJSON();
|
||||
final preferencesDumpFile = File('${tmpDir.path}/~_preferences_dump_TMP');
|
||||
final keychainDumpFile = File('${tmpDir.path}/~_keychain_dump_TMP');
|
||||
final transactionDescriptionDumpFile =
|
||||
File('${tmpDir.path}/~_transaction_descriptions_dump_TMP');
|
||||
|
||||
final transactionDescriptionData = _transactionDescriptionBox
|
||||
.toMap()
|
||||
.map((key, value) => MapEntry(key.toString(), value.toJson()));
|
||||
final transactionDescriptionDump = jsonEncode(transactionDescriptionData);
|
||||
|
||||
if (tmpDir.existsSync()) {
|
||||
tmpDir.deleteSync(recursive: true);
|
||||
}
|
||||
|
||||
tmpDir.createSync();
|
||||
zipEncoder.create(archivePath);
|
||||
|
||||
fileEntities.forEach((entity) {
|
||||
if (entity.path == archivePath || entity.path == tmpDir.path) {
|
||||
return;
|
||||
}
|
||||
final filename = entity.absolute;
|
||||
for (var ignore in ignoreFiles) {
|
||||
final filename = entity.absolute.path;
|
||||
if (filename.endsWith(ignore) && !filename.contains("wallets/")) {
|
||||
printV("ignoring backup file: $filename");
|
||||
return;
|
||||
}
|
||||
}
|
||||
printV("restoring: $filename");
|
||||
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 transactionDescriptionDumpFile.writeAsString(transactionDescriptionDump);
|
||||
await zipEncoder.addFile(preferencesDumpFile, '~_preferences_dump');
|
||||
await zipEncoder.addFile(keychainDumpFile, '~_keychain_dump');
|
||||
await zipEncoder.addFile(transactionDescriptionDumpFile, '~_transaction_descriptions_dump');
|
||||
zipEncoder.close();
|
||||
|
||||
final content = File(archivePath).readAsBytesSync();
|
||||
tmpDir.deleteSync(recursive: true);
|
||||
return await _encryptV2(content, password);
|
||||
}
|
||||
|
||||
Future<void> _importBackupV1(Uint8List data, String password, {required String nonce}) async {
|
||||
Future<void> importBackupV1(Uint8List data, String password, {required String nonce}) async {
|
||||
final appDir = await getAppDir();
|
||||
final decryptedData = await _decryptV1(data, password, nonce);
|
||||
final zip = ZipDecoder().decodeBytes(decryptedData);
|
||||
|
@ -153,9 +62,9 @@ class BackupService {
|
|||
}
|
||||
};
|
||||
|
||||
await _verifyWallets();
|
||||
await verifyWallets();
|
||||
await _importKeychainDumpV1(password, nonce: nonce);
|
||||
await _importPreferencesDump();
|
||||
await importPreferencesDump();
|
||||
}
|
||||
|
||||
// checked with .endsWith - so this should be the last part of the filename
|
||||
|
@ -163,12 +72,13 @@ class BackupService {
|
|||
"flutter_assets/kernel_blob.bin",
|
||||
"flutter_assets/vm_snapshot_data",
|
||||
"flutter_assets/isolate_snapshot_data",
|
||||
"README.txt",
|
||||
".lock",
|
||||
];
|
||||
|
||||
Future<void> _importBackupV2(Uint8List data, String password) async {
|
||||
Future<void> importBackupV2(Uint8List data, String password) async {
|
||||
final appDir = await getAppDir();
|
||||
final decryptedData = await _decryptV2(data, password);
|
||||
final decryptedData = await decryptV2(data, password);
|
||||
final zip = ZipDecoder().decodeBytes(decryptedData);
|
||||
|
||||
outer:
|
||||
|
@ -187,22 +97,25 @@ class BackupService {
|
|||
..createSync(recursive: true)
|
||||
..writeAsBytesSync(content, flush: true);
|
||||
} else {
|
||||
Directory('${appDir.path}/' + filename)..create(recursive: true);
|
||||
final dir = Directory('${appDir.path}/' + filename);
|
||||
if (!dir.existsSync()) {
|
||||
dir.createSync(recursive: true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await _verifyWallets();
|
||||
await _importKeychainDumpV2(password);
|
||||
await _importPreferencesDump();
|
||||
await _importTransactionDescriptionDump(); // HiveError: Box has already been closed
|
||||
await verifyWallets();
|
||||
await importKeychainDumpV2(password);
|
||||
await importPreferencesDump();
|
||||
await importTransactionDescriptionDump(); // HiveError: Box has already been closed
|
||||
}
|
||||
|
||||
Future<void> _verifyWallets() async {
|
||||
Future<void> verifyWallets() async {
|
||||
final walletInfoSource = await _reloadHiveWalletInfoBox();
|
||||
_correctWallets =
|
||||
correctWallets =
|
||||
walletInfoSource.values.where((info) => availableWalletTypes.contains(info.type)).toList();
|
||||
|
||||
if (_correctWallets.isEmpty) {
|
||||
if (correctWallets.isEmpty) {
|
||||
throw Exception('Correct wallets not detected');
|
||||
}
|
||||
}
|
||||
|
@ -219,7 +132,7 @@ class BackupService {
|
|||
return await CakeHive.openBox<WalletInfo>(WalletInfo.boxName);
|
||||
}
|
||||
|
||||
Future<void> _importTransactionDescriptionDump() async {
|
||||
Future<void> importTransactionDescriptionDump() async {
|
||||
final appDir = await getAppDir();
|
||||
final transactionDescriptionFile = File('${appDir.path}/~_transaction_descriptions_dump');
|
||||
|
||||
|
@ -231,7 +144,7 @@ class BackupService {
|
|||
json.decode(transactionDescriptionFile.readAsStringSync()) as Map<String, dynamic>;
|
||||
final descriptionsMap = jsonData.map((key, value) =>
|
||||
MapEntry(key, TransactionDescription.fromJson(value as Map<String, dynamic>)));
|
||||
var box = _transactionDescriptionBox;
|
||||
var box = transactionDescriptionBox;
|
||||
if (!box.isOpen) {
|
||||
final transactionDescriptionsBoxKey =
|
||||
await getEncryptionKey(secureStorage: _secureStorage, forKey: TransactionDescription.boxKey);
|
||||
|
@ -242,7 +155,7 @@ class BackupService {
|
|||
await box.putAll(descriptionsMap);
|
||||
}
|
||||
|
||||
Future<void> _importPreferencesDump() async {
|
||||
Future<void> importPreferencesDump() async {
|
||||
final appDir = await getAppDir();
|
||||
final preferencesFile = File('${appDir.path}/~_preferences_dump');
|
||||
|
||||
|
@ -254,12 +167,12 @@ class BackupService {
|
|||
String currentWalletName = data[PreferencesKey.currentWalletName] as String;
|
||||
int currentWalletType = data[PreferencesKey.currentWalletType] as int;
|
||||
|
||||
final isCorrentCurrentWallet = _correctWallets
|
||||
final isCorrentCurrentWallet = correctWallets
|
||||
.any((info) => info.name == currentWalletName && info.type.index == currentWalletType);
|
||||
|
||||
if (!isCorrentCurrentWallet) {
|
||||
currentWalletName = _correctWallets.first.name;
|
||||
currentWalletType = serializeToInt(_correctWallets.first.type);
|
||||
currentWalletName = correctWallets.first.name;
|
||||
currentWalletType = serializeToInt(correctWallets.first.type);
|
||||
}
|
||||
|
||||
final currentNodeId = data[PreferencesKey.currentNodeIdKey] as int?;
|
||||
|
@ -298,114 +211,114 @@ class BackupService {
|
|||
final autoGenerateSubaddressStatus =
|
||||
data[PreferencesKey.autoGenerateSubaddressStatusKey] as int?;
|
||||
|
||||
await _sharedPreferences.setString(PreferencesKey.currentWalletName, currentWalletName);
|
||||
await sharedPreferences.setString(PreferencesKey.currentWalletName, currentWalletName);
|
||||
|
||||
if (currentNodeId != null)
|
||||
await _sharedPreferences.setInt(PreferencesKey.currentNodeIdKey, currentNodeId);
|
||||
await sharedPreferences.setInt(PreferencesKey.currentNodeIdKey, currentNodeId);
|
||||
|
||||
if (currentBalanceDisplayMode != null)
|
||||
await _sharedPreferences.setInt(
|
||||
await sharedPreferences.setInt(
|
||||
PreferencesKey.currentBalanceDisplayModeKey, currentBalanceDisplayMode);
|
||||
|
||||
await _sharedPreferences.setInt(PreferencesKey.currentWalletType, currentWalletType);
|
||||
await sharedPreferences.setInt(PreferencesKey.currentWalletType, currentWalletType);
|
||||
|
||||
if (currentFiatCurrency != null)
|
||||
await _sharedPreferences.setString(
|
||||
await sharedPreferences.setString(
|
||||
PreferencesKey.currentFiatCurrencyKey, currentFiatCurrency);
|
||||
|
||||
if (shouldSaveRecipientAddress != null)
|
||||
await _sharedPreferences.setBool(
|
||||
await sharedPreferences.setBool(
|
||||
PreferencesKey.shouldSaveRecipientAddressKey, shouldSaveRecipientAddress);
|
||||
|
||||
if (isAppSecure != null)
|
||||
await _sharedPreferences.setBool(PreferencesKey.isAppSecureKey, isAppSecure);
|
||||
await sharedPreferences.setBool(PreferencesKey.isAppSecureKey, isAppSecure);
|
||||
|
||||
if (disableTradeOption != null)
|
||||
await _sharedPreferences.setBool(PreferencesKey.disableTradeOption, disableTradeOption);
|
||||
await sharedPreferences.setBool(PreferencesKey.disableTradeOption, disableTradeOption);
|
||||
|
||||
if (currentTransactionPriorityKeyLegacy != null)
|
||||
await _sharedPreferences.setInt(
|
||||
await sharedPreferences.setInt(
|
||||
PreferencesKey.currentTransactionPriorityKeyLegacy, currentTransactionPriorityKeyLegacy);
|
||||
|
||||
if (currentBitcoinElectrumSererId != null)
|
||||
await _sharedPreferences.setInt(
|
||||
await sharedPreferences.setInt(
|
||||
PreferencesKey.currentBitcoinElectrumSererIdKey, currentBitcoinElectrumSererId);
|
||||
|
||||
if (currentLanguageCode != null)
|
||||
await _sharedPreferences.setString(PreferencesKey.currentLanguageCode, currentLanguageCode);
|
||||
await sharedPreferences.setString(PreferencesKey.currentLanguageCode, currentLanguageCode);
|
||||
|
||||
if (displayActionListMode != null)
|
||||
await _sharedPreferences.setInt(
|
||||
await sharedPreferences.setInt(
|
||||
PreferencesKey.displayActionListModeKey, displayActionListMode);
|
||||
|
||||
if (fiatApiMode != null)
|
||||
await _sharedPreferences.setInt(PreferencesKey.currentFiatApiModeKey, fiatApiMode);
|
||||
await sharedPreferences.setInt(PreferencesKey.currentFiatApiModeKey, fiatApiMode);
|
||||
if (autoGenerateSubaddressStatus != null)
|
||||
await _sharedPreferences.setInt(
|
||||
await sharedPreferences.setInt(
|
||||
PreferencesKey.autoGenerateSubaddressStatusKey, autoGenerateSubaddressStatus);
|
||||
|
||||
if (currentTheme != null && DeviceInfo.instance.isMobile) {
|
||||
await _sharedPreferences.setInt(PreferencesKey.currentTheme, currentTheme);
|
||||
await sharedPreferences.setInt(PreferencesKey.currentTheme, currentTheme);
|
||||
// enforce dark theme on desktop platforms until the design is ready:
|
||||
} else if (DeviceInfo.instance.isDesktop) {
|
||||
await _sharedPreferences.setInt(PreferencesKey.currentTheme, ThemeList.darkTheme.raw);
|
||||
await sharedPreferences.setInt(PreferencesKey.currentTheme, ThemeList.darkTheme.raw);
|
||||
}
|
||||
|
||||
if (exchangeStatus != null)
|
||||
await _sharedPreferences.setInt(PreferencesKey.exchangeStatusKey, exchangeStatus);
|
||||
await sharedPreferences.setInt(PreferencesKey.exchangeStatusKey, exchangeStatus);
|
||||
|
||||
if (currentDefaultSettingsMigrationVersion != null)
|
||||
await _sharedPreferences.setInt(PreferencesKey.currentDefaultSettingsMigrationVersion,
|
||||
await sharedPreferences.setInt(PreferencesKey.currentDefaultSettingsMigrationVersion,
|
||||
currentDefaultSettingsMigrationVersion);
|
||||
|
||||
if (moneroTransactionPriority != null)
|
||||
await _sharedPreferences.setInt(
|
||||
await sharedPreferences.setInt(
|
||||
PreferencesKey.moneroTransactionPriority, moneroTransactionPriority);
|
||||
|
||||
if (bitcoinTransactionPriority != null)
|
||||
await _sharedPreferences.setInt(
|
||||
await sharedPreferences.setInt(
|
||||
PreferencesKey.bitcoinTransactionPriority, bitcoinTransactionPriority);
|
||||
|
||||
if (sortBalanceTokensBy != null)
|
||||
await _sharedPreferences.setInt(PreferencesKey.sortBalanceBy, sortBalanceTokensBy);
|
||||
await sharedPreferences.setInt(PreferencesKey.sortBalanceBy, sortBalanceTokensBy);
|
||||
|
||||
if (pinNativeTokenAtTop != null)
|
||||
await _sharedPreferences.setBool(PreferencesKey.pinNativeTokenAtTop, pinNativeTokenAtTop);
|
||||
await sharedPreferences.setBool(PreferencesKey.pinNativeTokenAtTop, pinNativeTokenAtTop);
|
||||
|
||||
if (useEtherscan != null)
|
||||
await _sharedPreferences.setBool(PreferencesKey.useEtherscan, useEtherscan);
|
||||
await sharedPreferences.setBool(PreferencesKey.useEtherscan, useEtherscan);
|
||||
|
||||
if (defaultNanoRep != null)
|
||||
await _sharedPreferences.setString(PreferencesKey.defaultNanoRep, defaultNanoRep);
|
||||
await sharedPreferences.setString(PreferencesKey.defaultNanoRep, defaultNanoRep);
|
||||
|
||||
if (defaultBananoRep != null)
|
||||
await _sharedPreferences.setString(PreferencesKey.defaultBananoRep, defaultBananoRep);
|
||||
await sharedPreferences.setString(PreferencesKey.defaultBananoRep, defaultBananoRep);
|
||||
|
||||
if (syncAll != null) await _sharedPreferences.setBool(PreferencesKey.syncAllKey, syncAll);
|
||||
if (syncAll != null) await sharedPreferences.setBool(PreferencesKey.syncAllKey, syncAll);
|
||||
if (lookupsTwitter != null)
|
||||
await _sharedPreferences.setBool(PreferencesKey.lookupsTwitter, lookupsTwitter);
|
||||
await sharedPreferences.setBool(PreferencesKey.lookupsTwitter, lookupsTwitter);
|
||||
|
||||
if (lookupsMastodon != null)
|
||||
await _sharedPreferences.setBool(PreferencesKey.lookupsMastodon, lookupsMastodon);
|
||||
await sharedPreferences.setBool(PreferencesKey.lookupsMastodon, lookupsMastodon);
|
||||
|
||||
if (lookupsYatService != null)
|
||||
await _sharedPreferences.setBool(PreferencesKey.lookupsYatService, lookupsYatService);
|
||||
await sharedPreferences.setBool(PreferencesKey.lookupsYatService, lookupsYatService);
|
||||
|
||||
if (lookupsUnstoppableDomains != null)
|
||||
await _sharedPreferences.setBool(
|
||||
await sharedPreferences.setBool(
|
||||
PreferencesKey.lookupsUnstoppableDomains, lookupsUnstoppableDomains);
|
||||
|
||||
if (lookupsOpenAlias != null)
|
||||
await _sharedPreferences.setBool(PreferencesKey.lookupsOpenAlias, lookupsOpenAlias);
|
||||
await sharedPreferences.setBool(PreferencesKey.lookupsOpenAlias, lookupsOpenAlias);
|
||||
|
||||
if (lookupsENS != null) await _sharedPreferences.setBool(PreferencesKey.lookupsENS, lookupsENS);
|
||||
if (lookupsENS != null) await sharedPreferences.setBool(PreferencesKey.lookupsENS, lookupsENS);
|
||||
|
||||
if (lookupsWellKnown != null)
|
||||
await _sharedPreferences.setBool(PreferencesKey.lookupsWellKnown, lookupsWellKnown);
|
||||
await sharedPreferences.setBool(PreferencesKey.lookupsWellKnown, lookupsWellKnown);
|
||||
|
||||
if (syncAll != null) await _sharedPreferences.setBool(PreferencesKey.syncAllKey, syncAll);
|
||||
if (syncAll != null) await sharedPreferences.setBool(PreferencesKey.syncAllKey, syncAll);
|
||||
|
||||
if (syncMode != null) await _sharedPreferences.setInt(PreferencesKey.syncModeKey, syncMode);
|
||||
if (syncMode != null) await sharedPreferences.setInt(PreferencesKey.syncModeKey, syncMode);
|
||||
|
||||
await preferencesFile.delete();
|
||||
}
|
||||
|
@ -436,12 +349,12 @@ class BackupService {
|
|||
keychainDumpFile.deleteSync();
|
||||
}
|
||||
|
||||
Future<void> _importKeychainDumpV2(String password,
|
||||
Future<void> importKeychainDumpV2(String password,
|
||||
{String keychainSalt = secrets.backupKeychainSalt}) async {
|
||||
final appDir = await getAppDir();
|
||||
final keychainDumpFile = File('${appDir.path}/~_keychain_dump');
|
||||
final decryptedKeychainDumpFileData =
|
||||
await _decryptV2(keychainDumpFile.readAsBytesSync(), '$keychainSalt$password');
|
||||
await decryptV2(keychainDumpFile.readAsBytesSync(), '$keychainSalt$password');
|
||||
final keychainJSON =
|
||||
json.decode(utf8.decode(decryptedKeychainDumpFileData)) as Map<String, dynamic>;
|
||||
final keychainWalletsInfo = keychainJSON['wallets'] as List;
|
||||
|
@ -462,7 +375,7 @@ class BackupService {
|
|||
final name = info['name'] as String;
|
||||
final password = info['password'] as String;
|
||||
|
||||
await _keyService.saveWalletPassword(walletName: name, password: password);
|
||||
await keyService.saveWalletPassword(walletName: name, password: password);
|
||||
}
|
||||
|
||||
@Deprecated('Use v2 instead')
|
||||
|
@ -470,14 +383,14 @@ class BackupService {
|
|||
{required String nonce, String keychainSalt = secrets.backupKeychainSalt}) async =>
|
||||
throw Exception('Deprecated');
|
||||
|
||||
Future<Uint8List> _exportKeychainDumpV2(String password,
|
||||
Future<Uint8List> exportKeychainDumpV2(String password,
|
||||
{String keychainSalt = secrets.backupKeychainSalt}) async {
|
||||
final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword);
|
||||
final wallets = await Future.wait(_walletInfoSource.values.map((walletInfo) async {
|
||||
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)
|
||||
'password': await keyService.getWalletPassword(walletName: walletInfo.name)
|
||||
};
|
||||
}));
|
||||
final backupPasswordKey = generateStoreKeyFor(key: SecretStoreKey.backupPassword);
|
||||
|
@ -489,58 +402,58 @@ class BackupService {
|
|||
return encrypted;
|
||||
}
|
||||
|
||||
Future<String> _exportPreferencesJSON() async {
|
||||
Future<String> exportPreferencesJSON() async {
|
||||
final preferences = <String, dynamic>{
|
||||
PreferencesKey.currentWalletName:
|
||||
_sharedPreferences.getString(PreferencesKey.currentWalletName),
|
||||
PreferencesKey.currentNodeIdKey: _sharedPreferences.getInt(PreferencesKey.currentNodeIdKey),
|
||||
sharedPreferences.getString(PreferencesKey.currentWalletName),
|
||||
PreferencesKey.currentNodeIdKey: sharedPreferences.getInt(PreferencesKey.currentNodeIdKey),
|
||||
PreferencesKey.currentBalanceDisplayModeKey:
|
||||
_sharedPreferences.getInt(PreferencesKey.currentBalanceDisplayModeKey),
|
||||
PreferencesKey.currentWalletType: _sharedPreferences.getInt(PreferencesKey.currentWalletType),
|
||||
sharedPreferences.getInt(PreferencesKey.currentBalanceDisplayModeKey),
|
||||
PreferencesKey.currentWalletType: sharedPreferences.getInt(PreferencesKey.currentWalletType),
|
||||
PreferencesKey.currentFiatCurrencyKey:
|
||||
_sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey),
|
||||
sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey),
|
||||
PreferencesKey.shouldSaveRecipientAddressKey:
|
||||
_sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey),
|
||||
PreferencesKey.disableTradeOption: _sharedPreferences.getBool(PreferencesKey.disableTradeOption),
|
||||
sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey),
|
||||
PreferencesKey.disableTradeOption: sharedPreferences.getBool(PreferencesKey.disableTradeOption),
|
||||
PreferencesKey.currentTransactionPriorityKeyLegacy:
|
||||
_sharedPreferences.getInt(PreferencesKey.currentTransactionPriorityKeyLegacy),
|
||||
sharedPreferences.getInt(PreferencesKey.currentTransactionPriorityKeyLegacy),
|
||||
PreferencesKey.currentBitcoinElectrumSererIdKey:
|
||||
_sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey),
|
||||
sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey),
|
||||
PreferencesKey.currentLanguageCode:
|
||||
_sharedPreferences.getString(PreferencesKey.currentLanguageCode),
|
||||
sharedPreferences.getString(PreferencesKey.currentLanguageCode),
|
||||
PreferencesKey.displayActionListModeKey:
|
||||
_sharedPreferences.getInt(PreferencesKey.displayActionListModeKey),
|
||||
PreferencesKey.currentTheme: _sharedPreferences.getInt(PreferencesKey.currentTheme),
|
||||
PreferencesKey.exchangeStatusKey: _sharedPreferences.getInt(PreferencesKey.exchangeStatusKey),
|
||||
sharedPreferences.getInt(PreferencesKey.displayActionListModeKey),
|
||||
PreferencesKey.currentTheme: sharedPreferences.getInt(PreferencesKey.currentTheme),
|
||||
PreferencesKey.exchangeStatusKey: sharedPreferences.getInt(PreferencesKey.exchangeStatusKey),
|
||||
PreferencesKey.currentDefaultSettingsMigrationVersion:
|
||||
_sharedPreferences.getInt(PreferencesKey.currentDefaultSettingsMigrationVersion),
|
||||
sharedPreferences.getInt(PreferencesKey.currentDefaultSettingsMigrationVersion),
|
||||
PreferencesKey.bitcoinTransactionPriority:
|
||||
_sharedPreferences.getInt(PreferencesKey.bitcoinTransactionPriority),
|
||||
sharedPreferences.getInt(PreferencesKey.bitcoinTransactionPriority),
|
||||
PreferencesKey.moneroTransactionPriority:
|
||||
_sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority),
|
||||
sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority),
|
||||
PreferencesKey.currentFiatApiModeKey:
|
||||
_sharedPreferences.getInt(PreferencesKey.currentFiatApiModeKey),
|
||||
PreferencesKey.sortBalanceBy: _sharedPreferences.getInt(PreferencesKey.sortBalanceBy),
|
||||
sharedPreferences.getInt(PreferencesKey.currentFiatApiModeKey),
|
||||
PreferencesKey.sortBalanceBy: sharedPreferences.getInt(PreferencesKey.sortBalanceBy),
|
||||
PreferencesKey.pinNativeTokenAtTop:
|
||||
_sharedPreferences.getBool(PreferencesKey.pinNativeTokenAtTop),
|
||||
PreferencesKey.useEtherscan: _sharedPreferences.getBool(PreferencesKey.useEtherscan),
|
||||
PreferencesKey.defaultNanoRep: _sharedPreferences.getString(PreferencesKey.defaultNanoRep),
|
||||
sharedPreferences.getBool(PreferencesKey.pinNativeTokenAtTop),
|
||||
PreferencesKey.useEtherscan: sharedPreferences.getBool(PreferencesKey.useEtherscan),
|
||||
PreferencesKey.defaultNanoRep: sharedPreferences.getString(PreferencesKey.defaultNanoRep),
|
||||
PreferencesKey.defaultBananoRep:
|
||||
_sharedPreferences.getString(PreferencesKey.defaultBananoRep),
|
||||
PreferencesKey.lookupsTwitter: _sharedPreferences.getBool(PreferencesKey.lookupsTwitter),
|
||||
PreferencesKey.lookupsMastodon: _sharedPreferences.getBool(PreferencesKey.lookupsMastodon),
|
||||
sharedPreferences.getString(PreferencesKey.defaultBananoRep),
|
||||
PreferencesKey.lookupsTwitter: sharedPreferences.getBool(PreferencesKey.lookupsTwitter),
|
||||
PreferencesKey.lookupsMastodon: sharedPreferences.getBool(PreferencesKey.lookupsMastodon),
|
||||
PreferencesKey.lookupsYatService:
|
||||
_sharedPreferences.getBool(PreferencesKey.lookupsYatService),
|
||||
sharedPreferences.getBool(PreferencesKey.lookupsYatService),
|
||||
PreferencesKey.lookupsUnstoppableDomains:
|
||||
_sharedPreferences.getBool(PreferencesKey.lookupsUnstoppableDomains),
|
||||
PreferencesKey.lookupsOpenAlias: _sharedPreferences.getBool(PreferencesKey.lookupsOpenAlias),
|
||||
PreferencesKey.lookupsENS: _sharedPreferences.getBool(PreferencesKey.lookupsENS),
|
||||
sharedPreferences.getBool(PreferencesKey.lookupsUnstoppableDomains),
|
||||
PreferencesKey.lookupsOpenAlias: sharedPreferences.getBool(PreferencesKey.lookupsOpenAlias),
|
||||
PreferencesKey.lookupsENS: sharedPreferences.getBool(PreferencesKey.lookupsENS),
|
||||
PreferencesKey.lookupsWellKnown:
|
||||
_sharedPreferences.getBool(PreferencesKey.lookupsWellKnown),
|
||||
PreferencesKey.syncModeKey: _sharedPreferences.getInt(PreferencesKey.syncModeKey),
|
||||
PreferencesKey.syncAllKey: _sharedPreferences.getBool(PreferencesKey.syncAllKey),
|
||||
sharedPreferences.getBool(PreferencesKey.lookupsWellKnown),
|
||||
PreferencesKey.syncModeKey: sharedPreferences.getInt(PreferencesKey.syncModeKey),
|
||||
PreferencesKey.syncAllKey: sharedPreferences.getBool(PreferencesKey.syncAllKey),
|
||||
PreferencesKey.autoGenerateSubaddressStatusKey:
|
||||
_sharedPreferences.getInt(PreferencesKey.autoGenerateSubaddressStatusKey),
|
||||
sharedPreferences.getInt(PreferencesKey.autoGenerateSubaddressStatusKey),
|
||||
};
|
||||
|
||||
return json.encode(preferences);
|
||||
|
@ -564,13 +477,13 @@ class BackupService {
|
|||
final nonce = base64.decode(nonceBase64).toList();
|
||||
final box = SecretBox(Uint8List.sublistView(data, 0, data.lengthInBytes - macLength).toList(),
|
||||
nonce: nonce, mac: Mac(Uint8List.sublistView(data, data.lengthInBytes - macLength)));
|
||||
final plainData = await _cipher.decrypt(box, secretKey: secretKey);
|
||||
final plainData = await cipher.decrypt(box, secretKey: secretKey);
|
||||
return Uint8List.fromList(plainData);
|
||||
}
|
||||
|
||||
Future<Uint8List> _encryptV2(Uint8List data, String passphrase) async =>
|
||||
cake_backup.encrypt(passphrase, data, version: _v2);
|
||||
|
||||
Future<Uint8List> _decryptV2(Uint8List data, String passphrase) async =>
|
||||
Future<Uint8List> decryptV2(Uint8List data, String passphrase) async =>
|
||||
cake_backup.decrypt(passphrase, data);
|
||||
}
|
||||
|
|
470
lib/core/backup_service_v3.dart
Normal file
470
lib/core/backup_service_v3.dart
Normal file
|
@ -0,0 +1,470 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:archive/archive_io.dart';
|
||||
import 'package:cake_wallet/core/backup_service.dart';
|
||||
import 'package:cake_wallet/.secrets.g.dart' as secrets;
|
||||
import 'package:cake_backup/backup.dart' as cake_backup;
|
||||
import 'package:cake_wallet/utils/package_info.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:cw_core/root_dir.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
enum BackupVersion {
|
||||
unknown, // index 0
|
||||
v1,
|
||||
v2,
|
||||
v3,
|
||||
}
|
||||
|
||||
class ChunkChecksum {
|
||||
ChunkChecksum({
|
||||
required this.encrypted,
|
||||
required this.plain,
|
||||
});
|
||||
|
||||
final String encrypted;
|
||||
final String plain;
|
||||
|
||||
factory ChunkChecksum.fromJson(Map<String, dynamic> json) {
|
||||
return ChunkChecksum(
|
||||
encrypted: json['encrypted'] as String,
|
||||
plain: json['plain'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'encrypted': encrypted,
|
||||
'plain': plain,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ChunkChecksum(encrypted: $encrypted, plain: $plain)';
|
||||
}
|
||||
}
|
||||
|
||||
class ChunkLength {
|
||||
ChunkLength({
|
||||
required this.encrypted,
|
||||
required this.plain,
|
||||
});
|
||||
|
||||
final int encrypted;
|
||||
final int plain;
|
||||
|
||||
factory ChunkLength.fromJson(Map<String, dynamic> json) {
|
||||
return ChunkLength(
|
||||
encrypted: json['encrypted'] as int,
|
||||
plain: json['plain'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'encrypted': encrypted,
|
||||
'plain': plain,
|
||||
};
|
||||
}
|
||||
@override
|
||||
String toString() {
|
||||
return 'ChunkLength(encrypted: $encrypted, plain: $plain)';
|
||||
}
|
||||
}
|
||||
|
||||
class ChunkDetails {
|
||||
ChunkDetails({
|
||||
required this.sha512sum,
|
||||
required this.length,
|
||||
});
|
||||
|
||||
final ChunkChecksum sha512sum;
|
||||
final ChunkLength length;
|
||||
|
||||
factory ChunkDetails.fromJson(Map<String, dynamic> json) {
|
||||
return ChunkDetails(
|
||||
sha512sum: ChunkChecksum.fromJson(json['sha512sum'] as Map<String, dynamic>),
|
||||
length: ChunkLength.fromJson(json['length'] as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'sha512sum': sha512sum,
|
||||
'length': length,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ChunkDetails(sha512sum: $sha512sum, length: $length)';
|
||||
}
|
||||
}
|
||||
|
||||
class BackupMetadata {
|
||||
BackupMetadata({
|
||||
required this.version,
|
||||
required this.sha512sum,
|
||||
required this.chunks,
|
||||
required this.cakeVersion,
|
||||
});
|
||||
|
||||
final BackupVersion version;
|
||||
String sha512sum;
|
||||
final List<ChunkDetails> chunks;
|
||||
String cakeVersion;
|
||||
factory BackupMetadata.fromJson(Map<String, dynamic> json) {
|
||||
return BackupMetadata(
|
||||
version: BackupVersion.values[json['version'] as int],
|
||||
sha512sum: json['sha512sum'] as String,
|
||||
chunks: (json['chunks'] as List<dynamic>).map((chunk) => ChunkDetails.fromJson(chunk as Map<String, dynamic>)).toList(),
|
||||
cakeVersion: json['cakeVersion'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'version': version.index,
|
||||
'sha512sum': sha512sum,
|
||||
'chunks': chunks.map((chunk) => chunk.toJson()).toList(),
|
||||
'cakeVersion': cakeVersion,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'BackupMetadata(version: $version, sha512sum: $sha512sum, chunks: $chunks)';
|
||||
}
|
||||
}
|
||||
|
||||
class BackupServiceV3 extends $BackupService {
|
||||
BackupServiceV3(super.secureStorage, super.walletInfoSource, super.transactionDescriptionBox, super.keyService, super.sharedPreferences);
|
||||
|
||||
static BackupVersion get currentVersion => BackupVersion.v3;
|
||||
|
||||
Future<File> exportBackupFile(String password, {String nonce = secrets.backupSalt}) {
|
||||
return exportBackupFileV3(password, nonce: nonce);
|
||||
}
|
||||
|
||||
BackupVersion getVersionFile(File data) {
|
||||
final raf = data.openSync(mode: FileMode.read);
|
||||
|
||||
try {
|
||||
// Read first 4 bytes to check both version and zip signature
|
||||
final buffer = Uint8List(1);
|
||||
final bytesRead = raf.readIntoSync(buffer);
|
||||
|
||||
if (bytesRead == 0) {
|
||||
throw Exception('Invalid backup file: empty file');
|
||||
}
|
||||
|
||||
// Check if first byte is version 1 or 2
|
||||
if (buffer[0] == 1) {
|
||||
return BackupVersion.v1;
|
||||
} else if (buffer[0] == 2) {
|
||||
return BackupVersion.v2;
|
||||
} else if (buffer[0] == 0x50) {
|
||||
// $ head -c 64 test-archive.zip | hexdump -C
|
||||
// 00000000 50 4b 03 04 ....
|
||||
// Here we just check if the first byte is the zip signature
|
||||
// Inside of v3 backup we have multiple files.
|
||||
// Check metadata.json for version in v3 backup
|
||||
final inputStream = InputFileStream(data.path);
|
||||
final archive = ZipDecoder().decodeStream(inputStream);
|
||||
final metadataFile = archive.findFile('metadata.json');
|
||||
if (metadataFile == null) {
|
||||
throw Exception('Invalid v3 backup: missing metadata.json');
|
||||
}
|
||||
final metadataBytes = metadataFile.rawContent!.readBytes();
|
||||
final metadataString = utf8.decode(metadataBytes);
|
||||
final metadataJsonRaw = json.decode(metadataString) as Map<String, dynamic>;
|
||||
final metadata = BackupMetadata.fromJson(metadataJsonRaw);
|
||||
if (metadata.version == BackupVersion.v3) {
|
||||
return BackupVersion.v3;
|
||||
}
|
||||
}
|
||||
|
||||
throw Exception('Invalid backup file: unknown version');
|
||||
} finally {
|
||||
raf.closeSync();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> importBackupFile(File file, String password, {String nonce = secrets.backupSalt}) {
|
||||
final version = getVersionFile(file);
|
||||
switch (version) {
|
||||
case BackupVersion.unknown:
|
||||
throw Exception('Invalid backup file: unknown version');
|
||||
case BackupVersion.v1:
|
||||
final data = file.readAsBytesSync();
|
||||
final backupBytes = data.toList()..removeAt(0);
|
||||
final backupData = Uint8List.fromList(backupBytes);
|
||||
return super.importBackupV1(backupData, password, nonce: nonce);
|
||||
case BackupVersion.v2:
|
||||
return super.importBackupV2(file.readAsBytesSync(), password);
|
||||
case BackupVersion.v3:
|
||||
return importBackupFileV3(file, password, nonce: nonce);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> importBackupFileV3(File file, String password, {String nonce = secrets.backupSalt}) async{
|
||||
// Overall design of v3 backup is the following:
|
||||
// 1. backup.zip - plaintext zip file that user can open with any archive manager
|
||||
// 2. backup.zip/README.txt - text file to let user know what is inside of this file
|
||||
// 3. backup.zip/metadata.json - json file with metadata about backup.
|
||||
// 4. backup.zip/data.bin - v2 backup file
|
||||
|
||||
final inputStream = InputFileStream(file.path);
|
||||
final archive = ZipDecoder().decodeStream(inputStream);
|
||||
final metadataFile = archive.findFile('metadata.json');
|
||||
if (metadataFile == null) {
|
||||
throw Exception('Invalid v3 backup: missing metadata.json');
|
||||
}
|
||||
final metadataBytes = metadataFile.rawContent!.readBytes();
|
||||
final metadataString = utf8.decode(metadataBytes);
|
||||
final metadataJsonRaw = json.decode(metadataString) as Map<String, dynamic>;
|
||||
final metadata = BackupMetadata.fromJson(metadataJsonRaw);
|
||||
|
||||
final dataFile = archive.findFile('data.bin');
|
||||
if (dataFile == null) {
|
||||
throw Exception('Invalid v3 backup: missing data.bin');
|
||||
}
|
||||
final dataStream = dataFile.rawContent!.getStream();
|
||||
|
||||
final decryptedData = File('${file.path}_decrypted'); // decrypted zip file
|
||||
if (decryptedData.existsSync()) {
|
||||
decryptedData.deleteSync();
|
||||
}
|
||||
decryptedData.createSync(recursive: true);
|
||||
decryptedData.writeAsBytesSync(Uint8List(0), mode: FileMode.write, flush: true);
|
||||
|
||||
int chunkIndex = 0;
|
||||
for (var chunk in metadata.chunks) {
|
||||
chunkIndex++;
|
||||
final chunkBytes = dataStream.readBytes(chunk.length.encrypted).toUint8List();
|
||||
final chunkChecksum = (await sha512.bind(Stream.fromIterable([chunkBytes])).first).toString();
|
||||
|
||||
// readBytes stores position internally, so we don't need to think about it.
|
||||
if (chunk.sha512sum.encrypted != chunkChecksum) {
|
||||
throw Exception('Invalid v3 backup: chunk (${chunk.length.encrypted} bytes) checksum mismatch at index $chunkIndex\n'
|
||||
'expected: ${chunk.sha512sum.encrypted}\n'
|
||||
'got: $chunkChecksum');
|
||||
}
|
||||
final decryptedChunk = await cake_backup.decrypt(password, chunkBytes);
|
||||
decryptedData.writeAsBytesSync(decryptedChunk, mode: FileMode.append, flush: true);
|
||||
}
|
||||
|
||||
|
||||
final sha512sum = (await sha512.bind(decryptedData.openRead()).first).toString();
|
||||
if (sha512sum.toString() != metadata.sha512sum) {
|
||||
throw Exception('Invalid v3 backup: SHA512 checksum mismatch\n'
|
||||
'expected: ${metadata.sha512sum}\n'
|
||||
'got: $sha512sum');
|
||||
}
|
||||
|
||||
// Decryption done, now we can import the backup (that is, unzip app data)
|
||||
|
||||
// archive is **NOT** backup, it is just a zip file that contains data.bin inside.
|
||||
// We need to unzip it to get the backup.
|
||||
// data.bin after decryption is available in decryptedData.
|
||||
|
||||
final zip = ZipDecoder();
|
||||
final decryptedDataStream = InputFileStream(decryptedData.path);
|
||||
final backupArchive = zip.decodeStream(decryptedDataStream);
|
||||
|
||||
|
||||
final appDir = await getAppDir();
|
||||
|
||||
outer:
|
||||
for (var file in backupArchive.files) {
|
||||
final filename = file.name;
|
||||
for (var ignore in $BackupService.ignoreFiles) {
|
||||
if (filename.endsWith(ignore) && !filename.contains("wallets/")) {
|
||||
printV("ignoring backup file: $filename");
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
printV("restoring: $filename");
|
||||
if (file.isFile) {
|
||||
final output = File('${appDir.path}/' + filename)
|
||||
..createSync(recursive: true);
|
||||
final outputStream = OutputFileStream(output.path);
|
||||
file.writeContent(outputStream);
|
||||
outputStream.flush();
|
||||
} else {
|
||||
final dir = Directory('${appDir.path}/' + filename);
|
||||
if (!dir.existsSync()) {
|
||||
dir.createSync(recursive: true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Continue importing the backup the old way
|
||||
await super.verifyWallets();
|
||||
await super.importKeychainDumpV2(password);
|
||||
await super.importPreferencesDump();
|
||||
await super.importTransactionDescriptionDump();
|
||||
|
||||
// Delete decrypted data file
|
||||
decryptedData.deleteSync();
|
||||
}
|
||||
|
||||
Future<File> exportBackupFileV3(String password, {String nonce = secrets.backupSalt}) async {
|
||||
final metadata = BackupMetadata(
|
||||
version: BackupVersion.v3,
|
||||
sha512sum: 'tbd',
|
||||
chunks: [],
|
||||
cakeVersion: 'tbd',
|
||||
);
|
||||
final zipEncoder = ZipFileEncoder();
|
||||
final appDir = await getAppDir();
|
||||
final now = DateTime.now().toIso8601String().replaceAll(':', '-');
|
||||
final tmpDir = Directory('${appDir.path}/~_BACKUP_TMP');
|
||||
final archivePath = '${tmpDir.path}/backup_${now}.tmp.zip';
|
||||
final archivePathExport = '${tmpDir.path}/backup_${now}.zip';
|
||||
final fileEntities = appDir.listSync(recursive: false);
|
||||
final keychainDump = await super.exportKeychainDumpV2(password);
|
||||
final preferencesDump = await super.exportPreferencesJSON();
|
||||
final preferencesDumpFile = File('${tmpDir.path}/~_preferences_dump_TMP');
|
||||
final keychainDumpFile = File('${tmpDir.path}/~_keychain_dump_TMP');
|
||||
final transactionDescriptionDumpFile =
|
||||
File('${tmpDir.path}/~_transaction_descriptions_dump_TMP');
|
||||
|
||||
final transactionDescriptionData = super.transactionDescriptionBox
|
||||
.toMap()
|
||||
.map((key, value) => MapEntry(key.toString(), value.toJson()));
|
||||
final transactionDescriptionDump = jsonEncode(transactionDescriptionData);
|
||||
|
||||
if (tmpDir.existsSync()) {
|
||||
tmpDir.deleteSync(recursive: true);
|
||||
}
|
||||
|
||||
tmpDir.createSync();
|
||||
zipEncoder.create(archivePath);
|
||||
outer:
|
||||
for (var entity in fileEntities) {
|
||||
if (entity.path == archivePath || entity.path == tmpDir.path) {
|
||||
continue;
|
||||
}
|
||||
for (var ignore in $BackupService.ignoreFiles) {
|
||||
final filename = entity.absolute.path;
|
||||
if (filename.endsWith(ignore) && !filename.contains("wallets/")) {
|
||||
printV("ignoring backup file: $filename");
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
|
||||
if (entity.statSync().type == FileSystemEntityType.directory) {
|
||||
await zipEncoder.addDirectory(Directory(entity.path));
|
||||
} else {
|
||||
await zipEncoder.addFile(File(entity.path));
|
||||
}
|
||||
}
|
||||
await keychainDumpFile.writeAsBytes(keychainDump.toList());
|
||||
await preferencesDumpFile.writeAsString(preferencesDump);
|
||||
await transactionDescriptionDumpFile.writeAsString(transactionDescriptionDump);
|
||||
await zipEncoder.addFile(preferencesDumpFile, '~_preferences_dump');
|
||||
await zipEncoder.addFile(keychainDumpFile, '~_keychain_dump');
|
||||
await zipEncoder.addFile(transactionDescriptionDumpFile, '~_transaction_descriptions_dump');
|
||||
await zipEncoder.close();
|
||||
|
||||
final dataBinUnencrypted = File(archivePath);
|
||||
|
||||
final dataBin = File('${tmpDir.path}/data.bin');
|
||||
dataBin.writeAsBytesSync(Uint8List(0), mode: FileMode.write, flush: true);
|
||||
final dataBinWriter = dataBin.openWrite();
|
||||
|
||||
printV("------ Backup stats ------");
|
||||
printV("Backup version: ${metadata.version}");
|
||||
printV("Backup size: ${await dataBinUnencrypted.length()}");
|
||||
printV("Backup chunks: ${(await dataBinUnencrypted.length()) / chunkSize}");
|
||||
printV("------ Backup stats ------");
|
||||
|
||||
int chunkIndex = 0;
|
||||
final stopwatch = Stopwatch()..start();
|
||||
printV("Starting backup encryption...");
|
||||
|
||||
metadata.sha512sum = (await sha512.bind(dataBinUnencrypted.openRead()).first).toString();
|
||||
|
||||
final raf = await dataBinUnencrypted.open();
|
||||
|
||||
|
||||
while (true) {
|
||||
printV("Reading chunk ${chunkIndex++}");
|
||||
|
||||
stopwatch.reset();
|
||||
final chunk = await raf.read(chunkSize);
|
||||
printV("Chunk read completed in ${stopwatch.elapsed}");
|
||||
printV("Chunk length: ${chunk.length} expected: $chunkSize");
|
||||
if (chunk.length == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
stopwatch.reset();
|
||||
final encryptedChunk = await cake_backup.encrypt(password, chunk);
|
||||
printV("Encryption completed in ${stopwatch.elapsed}");
|
||||
|
||||
stopwatch.reset();
|
||||
final sha512sumEncryptedChunk = await sha512.bind(Stream.fromIterable([encryptedChunk])).first;
|
||||
final sha512sumUnencryptedChunk = await sha512.bind(Stream.fromIterable([chunk])).first;
|
||||
printV("Hashing completed in ${stopwatch.elapsed}");
|
||||
|
||||
stopwatch.reset();
|
||||
dataBinWriter.add(encryptedChunk);
|
||||
metadata.chunks.add(ChunkDetails(
|
||||
sha512sum: ChunkChecksum(
|
||||
encrypted: sha512sumEncryptedChunk.toString(),
|
||||
plain: sha512sumUnencryptedChunk.toString(),
|
||||
),
|
||||
length: ChunkLength(
|
||||
encrypted: encryptedChunk.length,
|
||||
plain: chunk.length,
|
||||
),
|
||||
));
|
||||
|
||||
await dataBinWriter.flush();
|
||||
printV("Writing completed in ${stopwatch.elapsed}");
|
||||
}
|
||||
await raf.close();
|
||||
|
||||
// Give the file to the user
|
||||
|
||||
final metadataFile = File('${tmpDir.path}/metadata.json');
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
metadata.cakeVersion = packageInfo.version;
|
||||
|
||||
metadataFile.writeAsStringSync(JsonEncoder.withIndent(' ').convert(metadata.toJson()));
|
||||
final readmeFile = File('${tmpDir.path}/README.txt');
|
||||
readmeFile.writeAsStringSync('''This is a ${packageInfo.appName} backup. Do not modify this archive.
|
||||
|
||||
App version: ${packageInfo.version}
|
||||
|
||||
If you have any issues with this backup, please contact our in-app support.
|
||||
This backup was created on ${DateTime.now().toIso8601String()}
|
||||
''');
|
||||
final zip = ZipFileEncoder();
|
||||
zip.create(archivePathExport, level: 9);
|
||||
await zip.addFile(dataBin, 'data.bin');
|
||||
await zip.addFile(metadataFile, 'metadata.json');
|
||||
await zip.addFile(readmeFile, 'README.txt');
|
||||
await zip.close();
|
||||
// tmpDir.deleteSync(recursive: true);
|
||||
final file = File(archivePathExport);
|
||||
return file;
|
||||
}
|
||||
|
||||
static const chunkSize = 24 * 1024 * 1024; // 24MiB
|
||||
|
||||
File setVersionFile(File file, BackupVersion version) {
|
||||
if (version == BackupVersion.v3) return file; // v3 uses
|
||||
// helper function to call super.setVersion();
|
||||
final data = file.readAsBytesSync();
|
||||
super.setVersion(data, version.index);
|
||||
file.writeAsBytesSync(data);
|
||||
return file;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue