mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-26 06:08:40 +01:00
fix-monero-com-backup-restore-bug (#3339)
* reject backups from other apps * make compatibility check optional
This commit is contained in:
@@ -144,6 +144,21 @@ class BackupMetadata {
|
||||
}
|
||||
}
|
||||
|
||||
class IncompatibleBackupAppException implements Exception {
|
||||
IncompatibleBackupAppException({
|
||||
required this.sourceAppName,
|
||||
required this.currentAppName,
|
||||
});
|
||||
|
||||
final String sourceAppName;
|
||||
final String currentAppName;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'This backup was created in $sourceAppName and cannot be restored in $currentAppName.';
|
||||
}
|
||||
}
|
||||
|
||||
class BackupServiceV3 extends $BackupService {
|
||||
BackupServiceV3(super.secureStorage, super.transactionDescriptionBox, super.keyService, super.sharedPreferences);
|
||||
|
||||
@@ -197,7 +212,8 @@ class BackupServiceV3 extends $BackupService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> importBackupFile(File file, String password, {String nonce = secrets.backupSalt}) {
|
||||
Future<void> importBackupFile(File file, String password,
|
||||
{String nonce = secrets.backupSalt, bool checkBackupApp = true}) {
|
||||
final version = getVersionFile(file);
|
||||
switch (version) {
|
||||
case BackupVersion.unknown:
|
||||
@@ -210,11 +226,12 @@ class BackupServiceV3 extends $BackupService {
|
||||
case BackupVersion.v2:
|
||||
return super.importBackupV2(file.readAsBytesSync(), password);
|
||||
case BackupVersion.v3:
|
||||
return importBackupFileV3(file, password, nonce: nonce);
|
||||
return importBackupFileV3(file, password, nonce: nonce, checkBackupApp: checkBackupApp);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> importBackupFileV3(File file, String password, {String nonce = secrets.backupSalt}) async{
|
||||
Future<void> importBackupFileV3(File file, String password,
|
||||
{String nonce = secrets.backupSalt,bool checkBackupApp = true}) 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
|
||||
@@ -223,6 +240,11 @@ class BackupServiceV3 extends $BackupService {
|
||||
|
||||
final inputStream = InputFileStream(file.path);
|
||||
final archive = ZipDecoder().decodeStream(inputStream);
|
||||
|
||||
if (checkBackupApp) {
|
||||
await _throwIfBackupWasCreatedInAnotherApp(archive);
|
||||
}
|
||||
|
||||
final metadataFile = archive.findFile('metadata.json');
|
||||
if (metadataFile == null) {
|
||||
throw Exception('Invalid v3 backup: missing metadata.json');
|
||||
@@ -425,7 +447,7 @@ class BackupServiceV3 extends $BackupService {
|
||||
metadata.sha512sum = (await sha512.bind(dataBinUnencrypted.openRead()).first).toString();
|
||||
|
||||
final raf = await dataBinUnencrypted.open();
|
||||
|
||||
|
||||
|
||||
while (true) {
|
||||
printV("Reading chunk ${chunkIndex++}");
|
||||
@@ -491,6 +513,33 @@ This backup was created on ${DateTime.now().toIso8601String()}
|
||||
return file;
|
||||
}
|
||||
|
||||
Future<void> _throwIfBackupWasCreatedInAnotherApp(Archive archive) async {
|
||||
final readmeFile = archive.findFile('README.txt');
|
||||
if (readmeFile == null) return;
|
||||
|
||||
final readmeBytes = readmeFile.rawContent?.readBytes();
|
||||
if (readmeBytes == null) return;
|
||||
|
||||
final readmeString = utf8.decode(readmeBytes, allowMalformed: true);
|
||||
final sourceAppName = _extractBackupAppName(readmeString);
|
||||
if (sourceAppName == null) return;
|
||||
|
||||
|
||||
final currentAppName = (await PackageInfo.fromPlatform()).appName;
|
||||
if (sourceAppName == currentAppName) return;
|
||||
|
||||
|
||||
throw IncompatibleBackupAppException(
|
||||
sourceAppName: sourceAppName,
|
||||
currentAppName: currentAppName,
|
||||
);
|
||||
}
|
||||
|
||||
String? _extractBackupAppName(String readme) {
|
||||
final match = RegExp(r'^This is a (.+) backup\.', multiLine: true).firstMatch(readme);
|
||||
return match?.group(1)?.trim();
|
||||
}
|
||||
|
||||
static const chunkSize = 24 * 1024 * 1024; // 24MiB
|
||||
|
||||
File setVersionFile(File file, BackupVersion version) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:cake_wallet/core/backup_service_v3.dart';
|
||||
import 'package:cake_wallet/core/execution_state.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
|
||||
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
|
||||
import 'package:cake_wallet/utils/responsive_layout_util.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
@@ -18,6 +20,7 @@ class RestoreFromBackupPage extends BasePage {
|
||||
|
||||
final RestoreFromBackupViewModel restoreFromBackupViewModel;
|
||||
final TextEditingController textEditingController;
|
||||
bool _isStateReactionSet = false;
|
||||
|
||||
@override
|
||||
String get title => S.current.restore_title_from_backup;
|
||||
@@ -32,22 +35,26 @@ class RestoreFromBackupPage extends BasePage {
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) {
|
||||
reaction((_) => restoreFromBackupViewModel.state, (ExecutionState state) {
|
||||
if (state is FailureState) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertWithOneAction(
|
||||
alertTitle: S.of(context).error,
|
||||
alertContent: state.error,
|
||||
buttonText: S.of(context).ok,
|
||||
buttonAction: () => Navigator.of(context).pop(),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
if (!_isStateReactionSet) {
|
||||
_isStateReactionSet = true;
|
||||
|
||||
reaction((_) => restoreFromBackupViewModel.state, (ExecutionState state) {
|
||||
if (state is FailureState) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertWithOneAction(
|
||||
alertTitle: S.of(context).error,
|
||||
alertContent: state.error,
|
||||
buttonText: S.of(context).ok,
|
||||
buttonAction: () => Navigator.of(context).pop(),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
@@ -175,6 +182,29 @@ class RestoreFromBackupPage extends BasePage {
|
||||
}
|
||||
try {
|
||||
await restoreFromBackupViewModel.import(textEditingController.text);
|
||||
} on IncompatibleBackupAppException catch (e) {
|
||||
final isConfirmed = await showPopUp<bool>(
|
||||
context: context,
|
||||
builder: (_) {
|
||||
return AlertWithTwoActions(
|
||||
alertTitle: S.current.warning,
|
||||
alertContent:
|
||||
'This backup was created in ${e.sourceAppName}, but you are restoring it in ${e.currentAppName}. '
|
||||
'Please make sure this is the correct backup file before continuing.',
|
||||
leftButtonText: S.of(context).cancel,
|
||||
rightButtonText: S.of(context).ok,
|
||||
actionLeftButton: () => Navigator.of(context).pop(false),
|
||||
actionRightButton: () => Navigator.of(context).pop(true),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (isConfirmed ?? false) {
|
||||
await restoreFromBackupViewModel.import(
|
||||
textEditingController.text,
|
||||
checkBackupApp: false,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
|
||||
@@ -32,7 +32,7 @@ abstract class RestoreFromBackupViewModelBase with Store {
|
||||
void reset() => filePath = '';
|
||||
|
||||
@action
|
||||
Future<void> import(String password) async {
|
||||
Future<void> import(String password,{bool checkBackupApp = true}) async {
|
||||
try {
|
||||
state = IsExecutingState();
|
||||
|
||||
@@ -44,7 +44,9 @@ abstract class RestoreFromBackupViewModelBase with Store {
|
||||
final file = File(filePath);
|
||||
|
||||
try {
|
||||
await backupService.importBackupFile(file, password);
|
||||
await backupService.importBackupFile(file, password, checkBackupApp: checkBackupApp);
|
||||
} on IncompatibleBackupAppException {
|
||||
rethrow;
|
||||
} catch (e, s) {
|
||||
if (e.toString().contains("unknown_backup_version")) {
|
||||
state = FailureState('This is not a valid backup file, please make sure you have selected the correct one');
|
||||
@@ -74,6 +76,9 @@ abstract class RestoreFromBackupViewModelBase with Store {
|
||||
});
|
||||
|
||||
state = ExecutedSuccessfullyState();
|
||||
} on IncompatibleBackupAppException {
|
||||
state = InitialExecutionState();
|
||||
rethrow;
|
||||
} catch (e, s) {
|
||||
var msg = e.toString().toLowerCase();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user