Xchaha20 poly1305 integration (#569)

* Integration of xchacha20-poly1305 * Remove force unwrap from _exportPreferencesJSON * Deprecate v1 export * Fix for open backup screen after auth

mkyq committed Oct 26, 2022 at 15:28 UTC acb03e55309909d6a87dac273a43fbe26374397f
5 files changed +168 -53
lib/core/backup_service.dart
+112 -43
@@ -16,6 +16,7 @@ import 'package:cake_wallet/entities/secret_store_key.dart';
16 import 'package:cw_core/wallet_info.dart';
17 import 'package:cake_wallet/.secrets.g.dart' as secrets;
18 import 'package:cake_wallet/wallet_types.g.dart';
19 +import 'package:cake_backup/backup.dart' as cake_backup;
20
21 class BackupService {
22 BackupService(this._flutterSecureStorage, this._walletInfoSource,
@@ -23,9 +24,10 @@ class BackupService {
24 : _cipher = Cryptography.instance.chacha20Poly1305Aead(),
25 _correctWallets = <WalletInfo>[];
26
26 - static const currentVersion = _v1;
27 + static const currentVersion = _v2;
28
29 static const _v1 = 1;
30 + static const _v2 = 2;
31
32 final Cipher _cipher;
33 final FlutterSecureStorage _flutterSecureStorage;
@@ -37,13 +39,16 @@ class BackupService {
39 Future<void> importBackup(Uint8List data, String password,
40 {String nonce = secrets.backupSalt}) async {
41 final version = getVersion(data);
40 - final backupBytes = data.toList()..removeAt(0);
41 - final backupData = Uint8List.fromList(backupBytes);
42
43 switch (version) {
44 case _v1:
45 + final backupBytes = data.toList()..removeAt(0);
46 + final backupData = Uint8List.fromList(backupBytes);
47 await _importBackupV1(backupData, password, nonce: nonce);
48 break;
49 + case _v2:
50 + await _importBackupV2(data, password);
51 + break;
52 default:
53 break;
54 }
@@ -54,20 +59,26 @@ class BackupService {
59 switch (version) {
60 case _v1:
61 return await _exportBackupV1(password, nonce: nonce);
62 + case _v2:
63 + return await _exportBackupV2(password);
64 default:
65 throw Exception('Incorrect version: $version for exportBackup');
66 }
67 }
68
69 + @Deprecated('Use v2 instead')
70 Future<Uint8List> _exportBackupV1(String password,
63 - {String nonce = secrets.backupSalt}) async {
71 + {String nonce = secrets.backupSalt}) async
72 + => throw Exception('Deprecated. Export for backups v1 is deprecated. Please use export v2.');
73 +
74 + Future<Uint8List> _exportBackupV2(String password) async {
75 final zipEncoder = ZipFileEncoder();
76 final appDir = await getApplicationDocumentsDirectory();
77 final now = DateTime.now();
78 final tmpDir = Directory('${appDir.path}/~_BACKUP_TMP');
79 final archivePath = '${tmpDir.path}/backup_${now.toString()}.zip';
80 final fileEntities = appDir.listSync(recursive: false);
70 - final keychainDump = await _exportKeychainDump(password, nonce: nonce);
81 + final keychainDump = await _exportKeychainDumpV2(password);
82 final preferencesDump = await _exportPreferencesJSON();
83 final preferencesDumpFile = File('${tmpDir.path}/~_preferences_dump_TMP');
84 final keychainDumpFile = File('${tmpDir.path}/~_keychain_dump_TMP');
@@ -98,15 +109,13 @@ class BackupService {
109
110 final content = File(archivePath).readAsBytesSync();
111 tmpDir.deleteSync(recursive: true);
101 - final encryptedData = await _encrypt(content, password, nonce);
102 -
103 - return setVersion(encryptedData, currentVersion);
112 + return await _encryptV2(content, password);
113 }
114
115 Future<void> _importBackupV1(Uint8List data, String password,
116 {required String nonce}) async {
117 final appDir = await getApplicationDocumentsDirectory();
109 - final decryptedData = await _decrypt(data, password, nonce);
118 + final decryptedData = await _decryptV1(data, password, nonce);
119 final zip = ZipDecoder().decodeBytes(decryptedData);
120
121 zip.files.forEach((file) {
@@ -123,7 +132,30 @@ class BackupService {
132 });
133
134 await _verifyWallets();
126 - await _importKeychainDump(password, nonce: nonce);
135 + await _importKeychainDumpV1(password, nonce: nonce);
136 + await _importPreferencesDump();
137 + }
138 +
139 + Future<void> _importBackupV2(Uint8List data, String password) async {
140 + final appDir = await getApplicationDocumentsDirectory();
141 + final decryptedData = await _decryptV2(data, password);
142 + final zip = ZipDecoder().decodeBytes(decryptedData);
143 +
144 + zip.files.forEach((file) {
145 + final filename = file.name;
146 +
147 + if (file.isFile) {
148 + final content = file.content as List<int>;
149 + File('${appDir.path}/' + filename)
150 + ..createSync(recursive: true)
151 + ..writeAsBytesSync(content);
152 + } else {
153 + Directory('${appDir.path}/' + filename)..create(recursive: true);
154 + }
155 + });
156 +
157 + await _verifyWallets();
158 + await _importKeychainDumpV2(password);
159 await _importPreferencesDump();
160 }
161
@@ -258,12 +290,12 @@ class BackupService {
290 await preferencesFile.delete();
291 }
292
261 - Future<void> _importKeychainDump(String password,
293 + Future<void> _importKeychainDumpV1(String password,
294 {required String nonce,
295 String keychainSalt = secrets.backupKeychainSalt}) async {
296 final appDir = await getApplicationDocumentsDirectory();
297 final keychainDumpFile = File('${appDir.path}/~_keychain_dump');
266 - final decryptedKeychainDumpFileData = await _decrypt(
298 + final decryptedKeychainDumpFileData = await _decryptV1(
299 keychainDumpFile.readAsBytesSync(), '$keychainSalt$password', nonce);
300 final keychainJSON = json.decode(utf8.decode(decryptedKeychainDumpFileData))
301 as Map<String, dynamic>;
@@ -288,6 +320,35 @@ class BackupService {
320 keychainDumpFile.deleteSync();
321 }
322
323 + Future<void> _importKeychainDumpV2(String password,
324 + {String keychainSalt = secrets.backupKeychainSalt}) async {
325 + final appDir = await getApplicationDocumentsDirectory();
326 + final keychainDumpFile = File('${appDir.path}/~_keychain_dump');
327 + final decryptedKeychainDumpFileData = await _decryptV2(
328 + keychainDumpFile.readAsBytesSync(), '$keychainSalt$password');
329 + final keychainJSON = json.decode(utf8.decode(decryptedKeychainDumpFileData))
330 + as Map<String, dynamic>;
331 + final keychainWalletsInfo = keychainJSON['wallets'] as List;
332 + final decodedPin = keychainJSON['pin'] as String;
333 + final pinCodeKey = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword);
334 + final backupPasswordKey =
335 + generateStoreKeyFor(key: SecretStoreKey.backupPassword);
336 + final backupPassword = keychainJSON[backupPasswordKey] as String;
337 +
338 + await _flutterSecureStorage.write(
339 + key: backupPasswordKey, value: backupPassword);
340 +
341 + keychainWalletsInfo.forEach((dynamic rawInfo) async {
342 + final info = rawInfo as Map<String, dynamic>;
343 + await importWalletKeychainInfo(info);
344 + });
345 +
346 + await _flutterSecureStorage.write(
347 + key: pinCodeKey, value: encodedPinCode(pin: decodedPin));
348 +
349 + keychainDumpFile.deleteSync();
350 + }
351 +
352 Future<void> importWalletKeychainInfo(Map<String, dynamic> info) async {
353 final name = info['name'] as String;
354 final password = info['password'] as String;
@@ -295,9 +356,14 @@ class BackupService {
356 await _keyService.saveWalletPassword(walletName: name, password: password);
357 }
358
298 - Future<Uint8List> _exportKeychainDump(String password,
359 + @Deprecated('Use v2 instead')
360 + Future<Uint8List> _exportKeychainDumpV1(String password,
361 {required String nonce,
300 - String keychainSalt = secrets.backupKeychainSalt}) async {
362 + String keychainSalt = secrets.backupKeychainSalt}) async
363 + => throw Exception('Deprecated');
364 +
365 + Future<Uint8List> _exportKeychainDumpV2(String password,
366 + {String keychainSalt = secrets.backupKeychainSalt}) async {
367 final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword);
368 final encodedPin = await _flutterSecureStorage.read(key: key);
369 final decodedPin = decodedPinCode(pin: encodedPin!);
@@ -319,49 +385,48 @@ class BackupService {
385 'wallets': wallets,
386 backupPasswordKey: backupPassword
387 }));
322 - final encrypted = await _encrypt(
323 - Uint8List.fromList(data), '$keychainSalt$password', nonce);
388 + final encrypted = await _encryptV2(
389 + Uint8List.fromList(data), '$keychainSalt$password');
390
391 return encrypted;
392 }
393
394 Future<String> _exportPreferencesJSON() async {
329 - // FIX-ME: Force unwrap
395 final preferences = <String, dynamic>{
396 PreferencesKey.currentWalletName:
332 - _sharedPreferences.getString(PreferencesKey.currentWalletName)!,
397 + _sharedPreferences.getString(PreferencesKey.currentWalletName),
398 PreferencesKey.currentNodeIdKey:
334 - _sharedPreferences.getInt(PreferencesKey.currentNodeIdKey)!,
399 + _sharedPreferences.getInt(PreferencesKey.currentNodeIdKey),
400 PreferencesKey.currentBalanceDisplayModeKey: _sharedPreferences
336 - .getInt(PreferencesKey.currentBalanceDisplayModeKey)!,
401 + .getInt(PreferencesKey.currentBalanceDisplayModeKey),
402 PreferencesKey.currentWalletType:
338 - _sharedPreferences.getInt(PreferencesKey.currentWalletType)!,
403 + _sharedPreferences.getInt(PreferencesKey.currentWalletType),
404 PreferencesKey.currentFiatCurrencyKey:
340 - _sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey)!,
405 + _sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey),
406 PreferencesKey.shouldSaveRecipientAddressKey: _sharedPreferences
342 - .getBool(PreferencesKey.shouldSaveRecipientAddressKey)!,
407 + .getBool(PreferencesKey.shouldSaveRecipientAddressKey),
408 PreferencesKey.isDarkThemeLegacy:
344 - _sharedPreferences.getBool(PreferencesKey.isDarkThemeLegacy)!,
409 + _sharedPreferences.getBool(PreferencesKey.isDarkThemeLegacy),
410 PreferencesKey.currentPinLength:
346 - _sharedPreferences.getInt(PreferencesKey.currentPinLength)!,
411 + _sharedPreferences.getInt(PreferencesKey.currentPinLength),
412 PreferencesKey.currentTransactionPriorityKeyLegacy: _sharedPreferences
348 - .getInt(PreferencesKey.currentTransactionPriorityKeyLegacy)!,
413 + .getInt(PreferencesKey.currentTransactionPriorityKeyLegacy),
414 PreferencesKey.allowBiometricalAuthenticationKey: _sharedPreferences
350 - .getBool(PreferencesKey.allowBiometricalAuthenticationKey)!,
415 + .getBool(PreferencesKey.allowBiometricalAuthenticationKey),
416 PreferencesKey.currentBitcoinElectrumSererIdKey: _sharedPreferences
352 - .getInt(PreferencesKey.currentBitcoinElectrumSererIdKey)!,
417 + .getInt(PreferencesKey.currentBitcoinElectrumSererIdKey),
418 PreferencesKey.currentLanguageCode:
354 - _sharedPreferences.getString(PreferencesKey.currentLanguageCode)!,
419 + _sharedPreferences.getString(PreferencesKey.currentLanguageCode),
420 PreferencesKey.displayActionListModeKey:
356 - _sharedPreferences.getInt(PreferencesKey.displayActionListModeKey)!,
421 + _sharedPreferences.getInt(PreferencesKey.displayActionListModeKey),
422 PreferencesKey.currentTheme:
358 - _sharedPreferences.getInt(PreferencesKey.currentTheme)!,
423 + _sharedPreferences.getInt(PreferencesKey.currentTheme),
424 PreferencesKey.currentDefaultSettingsMigrationVersion: _sharedPreferences
360 - .getInt(PreferencesKey.currentDefaultSettingsMigrationVersion)!,
425 + .getInt(PreferencesKey.currentDefaultSettingsMigrationVersion),
426 PreferencesKey.bitcoinTransactionPriority:
362 - _sharedPreferences.getInt(PreferencesKey.bitcoinTransactionPriority)!,
427 + _sharedPreferences.getInt(PreferencesKey.bitcoinTransactionPriority),
428 PreferencesKey.moneroTransactionPriority:
364 - _sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority)!,
429 + _sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority),
430 };
431
432 return json.encode(preferences);
@@ -374,16 +439,12 @@ class BackupService {
439 return Uint8List.fromList(bytes);
440 }
441
377 - Future<Uint8List> _encrypt(
378 - Uint8List data, String secretKeySource, String nonceBase64) async {
379 - final secretKeyHash = await Cryptography.instance.sha256().hash(utf8.encode(secretKeySource));
380 - final secretKey = SecretKey(secretKeyHash.bytes);
381 - final nonce = base64.decode(nonceBase64).toList();
382 - final box = await _cipher.encrypt(data.toList(), secretKey: secretKey, nonce: nonce);
383 - return Uint8List.fromList(box.cipherText);
384 - }
442 + @Deprecated('Use v2 instead')
443 + Future<Uint8List> _encryptV1(
444 + Uint8List data, String secretKeySource, String nonceBase64) async
445 + => throw Exception('Deprecated');
446
386 - Future<Uint8List> _decrypt(
447 + Future<Uint8List> _decryptV1(
448 Uint8List data, String secretKeySource, String nonceBase64, {int macLength = 16}) async {
449 final secretKeyHash = await Cryptography.instance.sha256().hash(utf8.encode(secretKeySource));
450 final secretKey = SecretKey(secretKeyHash.bytes);
@@ -395,4 +456,12 @@ class BackupService {
456 final plainData = await _cipher.decrypt(box, secretKey: secretKey);
457 return Uint8List.fromList(plainData);
458 }
459 +
460 + Future<Uint8List> _encryptV2(
461 + Uint8List data, String passphrase) async
462 + => cake_backup.encrypt(passphrase, data, version: _v2);
463 +
464 + Future<Uint8List> _decryptV2(
465 + Uint8List data, String passphrase) async
466 + => cake_backup.decrypt(passphrase, data);
467 }
lib/src/screens/backup/backup_page.dart
+20 -10
@@ -1,10 +1,10 @@
1 import 'dart:io';
2 import 'package:cake_wallet/palette.dart';
3 import 'package:flutter/material.dart';
4 -import 'package:flutter/cupertino.dart';
4 import 'package:flutter/services.dart';
5 import 'package:flutter_mobx/flutter_mobx.dart';
7 -// import 'package:esys_flutter_share/esys_flutter_share.dart';
6 +import 'package:share_plus/share_plus.dart';
7 +import 'package:cross_file/cross_file.dart';
8 import 'package:cake_wallet/utils/show_bar.dart';
9 import 'package:cake_wallet/routes.dart';
10 import 'package:cake_wallet/generated/i18n.dart';
@@ -103,12 +103,14 @@ class BackupPage extends BasePage {
103 Navigator.of(dialogContext).pop();
104 final backup = await backupViewModelBase.exportBackup();
105
106 + if (backup == null) {
107 + return;
108 + }
109 +
110 if (Platform.isAndroid) {
107 - onExportAndroid(context, backup!);
111 + onExportAndroid(context, backup);
112 } else {
109 - // FIX-ME: Share esys_flutter_share.dart
110 - // await Share.file(S.of(context).backup_file, backup.name,
111 - // backup.content, 'application/*');
113 + await share(backup);
114 }
115 },
116 actionLeftButton: () => Navigator.of(dialogContext).pop());
@@ -136,12 +138,20 @@ class BackupPage extends BasePage {
138 backup.name, backup.content);
139 Navigator.of(dialogContext).pop();
140 },
139 - actionLeftButton: () {
141 + actionLeftButton: () async {
142 Navigator.of(dialogContext).pop();
141 - // FIX-ME: Share esys_flutter_share.dart
142 - // Share.file(S.of(context).backup_file, backup.name,
143 - // backup.content, 'application/*');
143 + await share(backup);
144 });
145 });
146 }
147 +
148 + Future<void> share(BackupExportFile backup) async {
149 + const mimeType = 'application/*';
150 + final path = await backupViewModelBase.saveBackupFileLocally(backup);
151 + await Share.shareXFiles(<XFile>[XFile(
152 + path,
153 + name: backup.name,
154 + mimeType: mimeType)]);
155 + await backupViewModelBase.removeBackupFileLocally(backup);
156 + }
157 }
lib/src/screens/dashboard/wallet_menu.dart
+15
@@ -51,6 +51,21 @@ class WalletMenu {
51 image: Image.asset('assets/images/open_book_menu.png',
52 height: 16, width: 16),
53 handler: () => Navigator.of(context).pushNamed(Routes.addressBook)),
54 + WalletMenuItem(
55 + title: S.current.backup,
56 + image: Image.asset('assets/images/restore_wallet.png',
57 + height: 16,
58 + width: 16,
59 + color: Palette.darkBlue),
60 + handler: () {
61 + Navigator.of(context).pushNamed(
62 + Routes.auth,
63 + arguments: (bool isAuthenticatedSuccessfully, AuthPageState auth) {
64 + if (isAuthenticatedSuccessfully) {
65 + auth.close(route:Routes.backup);
66 + }
67 + });
68 + }),
69 WalletMenuItem(
70 title: S.current.settings_title,
71 image: Image.asset('assets/images/settings_menu.png',
lib/view_model/backup_view_model.dart
+16
@@ -8,6 +8,7 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
8 import 'package:mobx/mobx.dart';
9 import 'package:intl/intl.dart';
10 import 'package:cake_wallet/wallet_type_utils.dart';
11 +import 'package:path_provider/path_provider.dart';
12
13 part 'backup_view_model.g.dart';
14
@@ -71,6 +72,21 @@ abstract class BackupViewModelBase with Store {
72 }
73 }
74
75 + Future<String> saveBackupFileLocally(BackupExportFile backup) async {
76 + final appDir = await getApplicationDocumentsDirectory();
77 + final path = '${appDir.path}/${backup.name}';
78 + final backupFile = File(path);
79 + await backupFile.writeAsBytes(backup.content);
80 + return path;
81 + }
82 +
83 + Future<void> removeBackupFileLocally(BackupExportFile backup) async {
84 + final appDir = await getApplicationDocumentsDirectory();
85 + final path = '${appDir.path}/${backup.name}';
86 + final backupFile = File(path);
87 + await backupFile.delete();
88 + }
89 +
90 @action
91 void showMasterPassword() => isBackupPasswordVisible = true;
92
pubspec_base.yaml
+5
@@ -61,6 +61,11 @@ dependencies:
61 permission_handler: ^10.0.0
62 device_display_brightness: ^0.0.6
63 platform_device_id: ^1.0.1
64 + cake_backup:
65 + git:
66 + url: https://github.com/cake-tech/cake_backup.git
67 + ref: main
68 + version: 1.0.0
69
70 dev_dependencies:
71 flutter_test: