Revert "fix: unify encryption across platforms (#3470)" (#3634)
What changed, and why it matters
This commit reverts a previous change that unified wallet file encryption across all platforms. It brings back two separate encryption paths: a newer XChaCha20-based method for direct user passwords, and an older Salsa20-based method for generated keys. The older Salsa20 path is not authenticated, meaning a wrong password can produce believable-looking garbage instead of failing cleanly. The revert also removes safety checks and migration logic that prevented accidental re-encryption of files with wrong passwords, and deletes the related security tests. Because this is a partial revert and the surrounding context is limited, the exact security intent is unclear, but the change reintroduces weaker, less-safe cryptography and removes test coverage for it.
Treat this commit as a potential security regression. Review the rationale for the revert with the vendor, re-run the deleted encryption tests, and ensure that any reintroduced Salsa20 usage is only transitional, clearly scoped, and does not handle new wallet data. If the revert was accidental or incomplete, re-apply the unified XChaCha20 encryption and restore the test suite. Advise users not to downgrade wallet files across this boundary until the security posture is clarified.
Security signals we found
Reverts a prior security-unification commit
Reintroduces Salsa20 stream cipher without authentication (no MAC)
Removes wrong-password detection logic that prevented silent garbage decryption
Removes migration logic that upgraded legacy Salsa20 files to authenticated XChaCha20
Deletes comprehensive encryption/security unit tests
Changes encryption behavior based on platform/direct-password flag
Rolls back pinned cake_backup dependency to earlier commit
Evidence from the diff
The commit reverts PR #3470 (‘fix: unify encryption across platforms’) via PR #3634. It replaces the unified XChaCha20MigratingEncryptionFileUtils with a split implementation: XChaCha20EncryptionFileUtils for direct passwords and Salsa20EncryhptionFileUtils for generated keys. The Salsa20 path uses the encrypt package’s Salsa20 stream cipher without a MAC, so decryption under an incorrect key yields random plaintext rather than an authentication failure. The reverted code had added checks (e.g., requiring decrypted legacy data to start with ‘{“’ and be valid JSON, and not rewriting files on wrong-password decryption) and migration logic to upgrade Salsa20 files to XChaCha20; this commit removes those checks and the entire encryption_file_utils_test.dart test suite. The wallet_service.dart and dashboard_view_model.dart call sites switch from encryptionFileUtilsFor(…) back to the raw read/write functions in utils/file.dart, which now use Salsa20 again. The cake_backup dependency is also rolled back from a pinned commit to an earlier/main commit.
Changed components
cw_core/lib/encryption_file_utils.dartcw_core/lib/key.dartcw_core/lib/utils/file.dartcw_core/lib/wallet_service.dartcw_core/pubspec.yamlcw_core/test/encryption_file_utils_test.dartlib/view_model/dashboard/dashboard_view_model.dartpubspec.lockpubspec_base.yamlpubspec_overrides.yamlInspect captured patch +86 / −555
diff --git a/cw_core/lib/encryption_file_utils.dart b/cw_core/lib/encryption_file_utils.dart
index 54729b73..72a601bc 100644
--- a/cw_core/lib/encryption_file_utils.dart
+++ b/cw_core/lib/encryption_file_utils.dart
@@ -1,34 +1,41 @@
+import 'dart:io';
+import 'dart:typed_data';
import 'package:cw_core/utils/file.dart' as file;
+import 'package:cake_backup/backup.dart' as cwb;
-EncryptionFileUtils encryptionFileUtilsFor(bool isDirect) =>
- XChaCha20MigratingEncryptionFileUtils(isDirect: isDirect);
+EncryptionFileUtils encryptionFileUtilsFor(bool direct) =>
+ direct ? XChaCha20EncryptionFileUtils() : Salsa20EncryhptionFileUtils();
abstract class EncryptionFileUtils {
Future<void> write({required String path, required String password, required String data});
Future<String> read({required String path, required String password});
}
-class XChaCha20MigratingEncryptionFileUtils extends EncryptionFileUtils {
- XChaCha20MigratingEncryptionFileUtils({required this.isDirect});
+class Salsa20EncryhptionFileUtils extends EncryptionFileUtils {
+ // Requires legacy complex key + iv as password
+ @override
+ Future<void> write(
+ {required String path, required String password, required String data}) async =>
+ await file.write(path: path, password: password, data: data);
- final bool isDirect;
+ // Requires legacy complex key + iv as password
+ @override
+ Future<String> read({required String path, required String password}) async =>
+ await file.read(path: path, password: password);
+}
+class XChaCha20EncryptionFileUtils extends EncryptionFileUtils {
@override
- Future<void> write({required String path, required String password, required String data}) {
- return file.write(
- path: path,
- password: password,
- data: data,
- highEntropyPassphrase: !isDirect,
- );
+ Future<void> write({required String path, required String password, required String data}) async {
+ final encrypted = await cwb.encrypt(password, Uint8List.fromList(data.codeUnits));
+ await File(path).writeAsBytes(encrypted);
}
@override
- Future<String> read({required String path, required String password}) {
- return file.read(
- path: path,
- password: password,
- highEntropyPassphrase: !isDirect,
- );
+ Future<String> read({required String path, required String password}) async {
+ final file = File(path);
+ final encrypted = await file.readAsBytes();
+ final bytes = await cwb.decrypt(password, encrypted);
+ return String.fromCharCodes(bytes);
}
}
diff --git a/cw_core/lib/key.dart b/cw_core/lib/key.dart
index 99570bc7..383cbfff 100644
--- a/cw_core/lib/key.dart
+++ b/cw_core/lib/key.dart
@@ -1,8 +1,35 @@
import 'package:encrypt/encrypt.dart' as encrypt;
+const ivEncodedStringLength = 12;
+
String generateKey() {
final key = encrypt.Key.fromSecureRandom(512);
final iv = encrypt.IV.fromSecureRandom(8);
return key.base64 + iv.base64;
}
+
+List<String> extractKeys(String key) {
+ final _key = key.substring(0, key.length - ivEncodedStringLength);
+ final iv = key.substring(key.length - ivEncodedStringLength);
+
+ return [_key, iv];
+}
+
+Future<String> encode(
+ {required encrypt.Key key, required encrypt.IV iv, required String data}) async {
+ final encrypter = encrypt.Encrypter(encrypt.Salsa20(key));
+ final encrypted = encrypter.encrypt(data, iv: iv);
+
+ return encrypted.base64;
+}
+
+Future<String> decode({required String password, required String data}) async {
+ final keys = extractKeys(password);
+ final key = encrypt.Key.fromBase64(keys.first);
+ final iv = encrypt.IV.fromBase64(keys.last);
+ final encrypter = encrypt.Encrypter(encrypt.Salsa20(key));
+ final encrypted = encrypter.decrypt64(data, iv: iv);
+
+ return encrypted;
+}
diff --git a/cw_core/lib/utils/file.dart b/cw_core/lib/utils/file.dart
index 3ad3165d..72fc9485 100644
--- a/cw_core/lib/utils/file.dart
+++ b/cw_core/lib/utils/file.dart
@@ -1,84 +1,21 @@
-import 'dart:convert';
import 'dart:io';
-import 'dart:typed_data';
-
-import 'package:cake_backup/backup.dart' as cwb;
+import 'package:cw_core/key.dart';
import 'package:encrypt/encrypt.dart' as encrypt;
-const _ivEncodedStringLength = 12;
-
-Future<void> write({
- required String path,
- required String password,
- required String data,
- bool? highEntropyPassphrase,
-}) async {
- await _writeXChaCha20(
- path: path,
- password: password,
- data: data,
- highEntropyPassphrase: highEntropyPassphrase ?? !Platform.isLinux,
- );
-}
-
-Future<String> read({
- required String path,
- required String password,
- bool? highEntropyPassphrase,
-}) async {
- final useHighEntropy = highEntropyPassphrase ?? !Platform.isLinux;
- try {
- return await _readXChaCha20(path: path, password: password);
- } catch (e) {
- final encrypted = await File(path).readAsBytes();
- if (encrypted.isNotEmpty &&
- (encrypted[0] == cwb.lowEntropyVersion || encrypted[0] == cwb.highEntropyVersion)) {
- rethrow;
- }
+Future<void> write({required String path, required String password, required String data}) async =>
+ writeData(path: path, password: password, data: data);
- final String data;
- try {
- data = await _readLegacy(path: path, password: password);
- } catch (_) {
- throw Exception('Failed to decrypt legacy file: invalid password or corrupted data');
- }
- if (data.isEmpty) {
- throw Exception('Failed to read data');
- }
-
- // Salsa20 is unauthenticated, so a wrong password decrypts to garbage instead of
- // failing. Every payload written here is a JSON object with at least one key, so a
- // correctly decrypted file always starts with '{"' and anything else is a bad
- // password.
- // There's 1 in 2^16 chance that this will be a false positive, but check later
- // prevents any damage to the file caused by re-encryption.
- if (!data.startsWith('{"')) {
- throw Exception('Failed to decrypt legacy file: invalid password or corrupted data');
- }
-
- if (_isJson(data)) {
- await _writeXChaCha20(
- path: path,
- password: password,
- data: data,
- highEntropyPassphrase: useHighEntropy,
- );
- }
-
- return data;
- }
-}
-
-bool _isJson(String data) {
- try {
- json.decode(data);
- return true;
- } catch (_) {
- return false;
- }
+Future<void> writeData(
+ {required String path, required String password, required String data}) async {
+ final keys = extractKeys(password);
+ final key = encrypt.Key.fromBase64(keys.first);
+ final iv = encrypt.IV.fromBase64(keys.last);
+ final encrypted = await encode(key: key, iv: iv, data: data);
+ final f = File(path);
+ f.writeAsStringSync(encrypted);
}
-Future<String> _readLegacy({required String path, required String password}) async {
+Future<String> read({required String path, required String password}) async {
final file = File(path);
if (!file.existsSync()) {
@@ -87,57 +24,5 @@ Future<String> _readLegacy({required String path, required String password}) asy
final encrypted = file.readAsStringSync();
- return _decode(password: password, data: encrypted);
-}
-
-Future<void> _writeXChaCha20({
- required String path,
- required String password,
- required String data,
- required bool highEntropyPassphrase,
-}) async {
- final encrypted = await cwb.encrypt(
- password,
- Uint8List.fromList(utf8.encode(data)),
- highEntropyPassphrase: highEntropyPassphrase,
- );
-
- final tmpFile = File('$path.tmp');
- tmpFile.writeAsBytesSync(encrypted, flush: true);
- tmpFile.renameSync(path);
-}
-
-Future<String> _readXChaCha20({
- required String path,
- required String password,
-}) async {
- final encrypted = await File(path).readAsBytes();
- final bytes = await cwb.decrypt(password, encrypted);
- return _decodeUtf8(bytes);
-}
-
-// Linux wallets written by the old XChaCha20EncryptionFileUtils stored one byte per
-// UTF-16 code unit, so bytes above 0x7F in those files are Latin-1 rather than UTF-8.
-String _decodeUtf8(Uint8List bytes) {
- try {
- return utf8.decode(bytes);
- } on FormatException {
- return String.fromCharCodes(bytes);
- }
-}
-
-List<String> _extractKeys(String key) {
- final k = key.substring(0, key.length - _ivEncodedStringLength);
- final iv = key.substring(key.length - _ivEncodedStringLength);
-
- return [k, iv];
-}
-
-Future<String> _decode({required String password, required String data}) async {
- final keys = _extractKeys(password);
- final key = encrypt.Key.fromBase64(keys.first);
- final iv = encrypt.IV.fromBase64(keys.last);
- final encrypter = encrypt.Encrypter(encrypt.Salsa20(key));
-
- return encrypter.decrypt64(data, iv: iv);
+ return decode(password: password, data: encrypted);
}
diff --git a/cw_core/lib/wallet_service.dart b/cw_core/lib/wallet_service.dart
index d69300d1..72ea98a9 100644
--- a/cw_core/lib/wallet_service.dart
+++ b/cw_core/lib/wallet_service.dart
@@ -1,18 +1,17 @@
import "dart:convert";
import "dart:io";
-import 'package:cw_core/encryption_file_utils.dart';
-import 'package:cw_core/imported_nft.dart';
-import 'package:cw_core/pathForWallet.dart';
-import 'package:cw_core/spl_token.dart';
-import 'package:cw_core/tron_token.dart';
-import 'package:cw_core/utils/print_verbose.dart';
-import 'package:cw_core/wallet_keys_file.dart';
-import 'package:cw_core/wallet_base.dart';
-import 'package:cw_core/wallet_credentials.dart';
-import 'package:cw_core/wallet_info.dart';
-import 'package:cw_core/wallet_type.dart';
-import 'package:path/path.dart' as p;
+import "package:cw_core/imported_nft.dart";
+import "package:cw_core/pathForWallet.dart";
+import "package:cw_core/spl_token.dart";
+import "package:cw_core/tron_token.dart";
+import "package:cw_core/utils/file.dart";
+import "package:cw_core/utils/print_verbose.dart";
+import "package:cw_core/wallet_base.dart";
+import "package:cw_core/wallet_credentials.dart";
+import "package:cw_core/wallet_info.dart";
+import "package:cw_core/wallet_type.dart";
+import "package:path/path.dart" as p;
abstract class WalletService<N extends WalletCredentials, RFS extends WalletCredentials,
RFK extends WalletCredentials, RFH extends WalletCredentials> {
@@ -96,15 +95,8 @@ abstract class WalletService<N extends WalletCredentials, RFS extends WalletCred
Future<String> getSeeds(String name, String password, WalletType type) async {
try {
- final encryption = encryptionFileUtilsFor(Platform.isLinux);
-
- if (await WalletKeysFile.hasKeysFile(name, type)) {
- final keysData = await WalletKeysFile.readKeysFile(name, type, password, encryption);
- return keysData.mnemonic ?? keysData.altMnemonic ?? keysData.privateKey ?? '';
- }
-
final path = await pathForWallet(name: name, type: type);
- final jsonSource = await encryption.read(path: path, password: password);
+ final jsonSource = await read(path: path, password: password);
try {
final data = json.decode(jsonSource) as Map;
return data["mnemonic"] as String? ?? "";
diff --git a/cw_core/pubspec.yaml b/cw_core/pubspec.yaml
index b7843f4b..38a4e9a4 100644
--- a/cw_core/pubspec.yaml
+++ b/cw_core/pubspec.yaml
@@ -22,7 +22,8 @@ dependencies:
cake_backup:
git:
url: https://github.com/cake-tech/cake_backup.git
- ref: b5d86a2a21e1c186cd0baa4df1f3a4f3f9413056
+ ref: main
+ version: 1.0.0
socks5_proxy:
git:
url: https://github.com/LacticWhale/socks_dart
diff --git a/cw_core/test/encryption_file_utils_test.dart b/cw_core/test/encryption_file_utils_test.dart
deleted file mode 100644
index bfd840f0..00000000
--- a/cw_core/test/encryption_file_utils_test.dart
+++ /dev/null
@@ -1,380 +0,0 @@
-import 'dart:convert';
-import 'dart:io';
-import 'dart:typed_data';
-
-import 'package:cake_backup/backup.dart' as cwb;
-import 'package:cw_core/encryption_file_utils.dart';
-import 'package:cw_core/key.dart';
-import 'package:cw_core/utils/file.dart' as encrypted_file;
-import 'package:cw_core/wallet_keys_file.dart';
-import 'package:encrypt/encrypt.dart' as encrypt;
-import 'package:flutter_test/flutter_test.dart';
-
-void main() {
- late Directory tmpDir;
-
- setUp(() {
- tmpDir = Directory.systemTemp.createTempSync('cw_enc_');
- });
-
- tearDown(() {
- if (tmpDir.existsSync()) {
- tmpDir.deleteSync(recursive: true);
- }
- });
-
- String path([String name = 'wallet.json']) => '${tmpDir.path}/$name';
-
- const walletJson = '{"mnemonic":"abandon ability able","privateKey":null}';
-
- group('XChaCha20 round-trip', () {
- test('writes and reads JSON with a generated high-entropy password', () async {
- final password = generateKey();
- await encrypted_file.write(
- path: path(),
- password: password,
- data: walletJson,
- highEntropyPassphrase: true,
- );
-
- expect(await encrypted_file.read(path: path(), password: password), walletJson);
- expect(File('${path()}.tmp').existsSync(), isFalse);
- });
-
- test('uses high-entropy (v3) vs low-entropy (v2) version bytes', () async {
- final password = generateKey();
-
- await encrypted_file.write(
- path: path('high'),
- password: password,
- data: walletJson,
- highEntropyPassphrase: true,
- );
- expect(File(path('high')).readAsBytesSync().first, cwb.highEntropyVersion);
-
- await encrypted_file.write(
- path: path('low'),
- password: password,
- data: walletJson,
- highEntropyPassphrase: false,
- );
- expect(File(path('low')).readAsBytesSync().first, cwb.lowEntropyVersion);
-
- expect(await encrypted_file.read(path: path('high'), password: password), walletJson);
- expect(await encrypted_file.read(path: path('low'), password: password), walletJson);
- });
-
- test('read ignores highEntropyPassphrase because version is in the file', () async {
- final password = generateKey();
- await encrypted_file.write(
- path: path(),
- password: password,
- data: walletJson,
- highEntropyPassphrase: true,
- );
-
- expect(
- await encrypted_file.read(
- path: path(),
- password: password,
- highEntropyPassphrase: false,
- ),
- walletJson,
- );
- });
-
- test('round-trips UTF-8 JSON with non-ASCII characters', () async {
- final password = generateKey();
- const data = '{"name":"Café","mnemonic":"abandon"}';
- await encrypted_file.write(
- path: path(),
- password: password,
- data: data,
- highEntropyPassphrase: true,
- );
-
- expect(await encrypted_file.read(path: path(), password: password), data);
- });
-
- test('wrong password does not fall back to Salsa20 or rewrite the file', () async {
- final password = generateKey();
- await encrypted_file.write(
- path: path(),
- password: password,
- data: walletJson,
- highEntropyPassphrase: true,
- );
- final before = File(path()).readAsBytesSync();
-
- await expectLater(
- encrypted_file.read(path: path(), password: generateKey()),
- throwsA(anything),
- );
- expect(File(path()).readAsBytesSync(), before);
- });
-
- test('corrupt XChaCha20 blob is not treated as a legacy Salsa20 file', () async {
- final password = generateKey();
- await encrypted_file.write(
- path: path(),
- password: password,
- data: walletJson,
- highEntropyPassphrase: true,
- );
- final bytes = File(path()).readAsBytesSync();
- bytes[bytes.length - 1] = bytes[bytes.length - 1] ^ 0xFF;
- File(path()).writeAsBytesSync(bytes);
-
- await expectLater(
- encrypted_file.read(path: path(), password: password),
- throwsA(anything),
- );
- expect(File(path()).readAsBytesSync().first, cwb.highEntropyVersion);
- });
- });
-
- group('encryptionFileUtilsFor', () {
- test('isDirect=false (generated password) writes high-entropy v3', () async {
- final password = generateKey();
- final encryption = encryptionFileUtilsFor(false);
- await encryption.write(path: path(), password: password, data: walletJson);
-
- expect(File(path()).readAsBytesSync().first, cwb.highEntropyVersion);
- expect(await encryption.read(path: path(), password: password), walletJson);
- });
-
- test('isDirect=true (user password) writes low-entropy v2', () async {
- const password = 'user-chosen-passphrase';
- final encryption = encryptionFileUtilsFor(true);
- await encryption.write(path: path(), password: password, data: walletJson);
-
- expect(File(path()).readAsBytesSync().first, cwb.lowEntropyVersion);
- expect(await encryption.read(path: path(), password: password), walletJson);
- });
-
- test('WalletKeysData JSON survives a write/read used by getSeeds', () async {
- final password = generateKey();
- final keys = WalletKeysData(
- mnemonic: 'abandon ability able about above absent absorb abstract',
- privateKey: 'deadbeef',
- );
- final encryption = encryptionFileUtilsFor(false);
- await encryption.write(path: path(), password: password, data: keys.toJSON());
-
- final decoded = json.decode(await encryption.read(path: path(), password: password))
- as Map<String, dynamic>;
- final restored = WalletKeysData.fromJSON(decoded);
- expect(restored.mnemonic, keys.mnemonic);
- expect(restored.privateKey, keys.privateKey);
- });
- });
-
- group('legacy Salsa20 migration', () {
- test('reads a Salsa20 wallet file and re-encrypts it as XChaCha20', () async {
- final password = generateKey();
- File(path()).writeAsStringSync(_salsa20Encrypt(password, walletJson));
- expect(File(path()).readAsBytesSync().first, isNot(cwb.lowEntropyVersion));
- expect(File(path()).readAsBytesSync().first, isNot(cwb.highEntropyVersion));
-
- final result = await encrypted_file.read(
- path: path(),
- password: password,
- highEntropyPassphrase: true,
- );
-
- expect(result, walletJson);
- expect(File(path()).readAsBytesSync().first, cwb.highEntropyVersion);
- expect(await encrypted_file.read(path: path(), password: password), walletJson);
- });
-
- test('migrated file can be opened with EncryptionFileUtils', () async {
- final password = generateKey();
- File(path()).writeAsStringSync(_salsa20Encrypt(password, walletJson));
-
- final encryption = encryptionFileUtilsFor(false);
- expect(await encryption.read(path: path(), password: password), walletJson);
- expect(await encryption.read(path: path(), password: password), walletJson);
- });
-
- test('wrong password does not rewrite the Salsa20 file', () async {
- final password = generateKey();
- final salsa = _salsa20Encrypt(password, walletJson);
- File(path()).writeAsStringSync(salsa);
-
- await expectLater(
- encrypted_file.read(path: path(), password: generateKey()),
- throwsLegacyDecryptError,
- );
- expect(File(path()).readAsStringSync(), salsa);
- });
-
- test('short or malformed password does not leak _readLegacy errors', () async {
- final password = generateKey();
- final salsa = _salsa20Encrypt(password, walletJson);
- File(path()).writeAsStringSync(salsa);
-
- await expectLater(
- encrypted_file.read(path: path(), password: 'user-pass'),
- throwsLegacyDecryptError,
- );
- expect(File(path()).readAsStringSync(), salsa);
- });
-
- test('corrupt non-base64 file does not leak _readLegacy errors', () async {
- File(path()).writeAsStringSync('not-valid-base64!!!');
-
- await expectLater(
- encrypted_file.read(path: path(), password: generateKey()),
- throwsLegacyDecryptError,
- );
- expect(File(path()).readAsStringSync(), 'not-valid-base64!!!');
- });
-
- test('invalid UTF-8 legacy bytes do not leak _readLegacy errors', () async {
- File(path()).writeAsBytesSync(const [0x00, 0xFF, 0xFE, 0x01]);
-
- await expectLater(
- encrypted_file.read(path: path(), password: generateKey()),
- throwsLegacyDecryptError,
- );
- expect(File(path()).readAsBytesSync(), const [0x00, 0xFF, 0xFE, 0x01]);
- });
-
- test('rejects Salsa20 plaintext that does not start with {"', () async {
- final password = generateKey();
- File(path()).writeAsStringSync(_salsa20Encrypt(password, 'not-json-payload'));
-
- await expectLater(
- encrypted_file.read(path: path(), password: password),
- throwsLegacyDecryptError,
- );
- expect(File(path()).readAsBytesSync().first, isNot(cwb.highEntropyVersion));
- expect(File(path()).readAsBytesSync().first, isNot(cwb.lowEntropyVersion));
- });
-
- test('does not re-encrypt Salsa20 data that starts with {" but is not JSON', () async {
- final password = generateKey();
- const garbage = '{"not valid json';
- File(path()).writeAsStringSync(_salsa20Encrypt(password, garbage));
-
- expect(await encrypted_file.read(path: path(), password: password), garbage);
- expect(File(path()).readAsBytesSync().first, isNot(cwb.highEntropyVersion));
- expect(File(path()).readAsBytesSync().first, isNot(cwb.lowEntropyVersion));
- });
-
- test(
- 'wrong password that decrypts to {" does not rewrite the file',
- () async {
- final password = generateKey();
- final salsa = _salsa20Encrypt(password, walletJson);
- File(path()).writeAsStringSync(salsa);
-
- final collision = _findWrongPasswordWithJsonObjectPrefix(salsa);
- expect(collision, isNot(password));
- expect(_salsa20Decrypt(collision, salsa).startsWith('{"'), isTrue);
- expect(() => json.decode(_salsa20Decrypt(collision, salsa)), throwsA(anything));
-
- final result = await encrypted_file.read(path: path(), password: collision);
-
- expect(result.startsWith('{"'), isTrue);
- expect(result, isNot(walletJson));
- expect(File(path()).readAsStringSync(), salsa);
- },
- );
- });
-
- group('legacy Linux XChaCha20 Latin-1 payloads', () {
- test('decodes files stored as one byte per UTF-16 code unit', () async {
- final password = generateKey();
- const data = '{"name":"Café","mnemonic":"abandon"}';
- // Old XChaCha20EncryptionFileUtils used String.codeUnits instead of utf8.encode.
- final encrypted = await cwb.encrypt(
- password,
- Uint8List.fromList(data.codeUnits),
- highEntropyPassphrase: true,
- );
- File(path()).writeAsBytesSync(encrypted);
-
- expect(await encrypted_file.read(path: path(), password: password), data);
- });
- });
-
- group('generateKey', () {
- test('produces unique passwords that still encrypt and decrypt', () async {
- final a = generateKey();
- final b = generateKey();
- expect(a, isNot(b));
- expect(a.length, greaterThan(12));
-
- await encrypted_file.write(
- path: path(),
- password: a,
- data: walletJson,
- highEntropyPassphrase: true,
- );
- expect(await encrypted_file.read(path: path(), password: a), walletJson);
- });
- });
-}
-
-final throwsLegacyDecryptError = throwsA(
- isA<Exception>().having(
- (e) => e.toString(),
- 'message',
- contains('Failed to decrypt legacy file'),
- ),
-);
-
-({encrypt.Key key, encrypt.IV iv}) _salsa20Keys(String password) {
- const ivEncodedStringLength = 12;
- return (
- key: encrypt.Key.fromBase64(password.substring(0, password.length - ivEncodedStringLength)),
- iv: encrypt.IV.fromBase64(password.substring(password.length - ivEncodedStringLength)),
- );
-}
-
-encrypt.Encrypter _salsa20(String password) {
- final keys = _salsa20Keys(password);
- return encrypt.Encrypter(encrypt.Salsa20(keys.key));
-}
-
-String _salsa20Encrypt(String password, String data) {
- final keys = _salsa20Keys(password);
- return _salsa20(password).encrypt(data, iv: keys.iv).base64;
-}
-
-String _salsa20Decrypt(String password, String data) {
- final keys = _salsa20Keys(password);
- return _salsa20(password).decrypt64(data, iv: keys.iv);
-}
-
-List<int> _salsa20DecryptBytes(String password, String data) {
- final keys = _salsa20Keys(password);
- return _salsa20(password).decryptBytes(encrypt.Encrypted.fromBase64(data), iv: keys.iv);
-}
-
-String _passwordFromNonce(int nonce) {
- final ivBytes = Uint8List(8);
- ivBytes.buffer.asByteData().setUint64(0, nonce, Endian.little);
- return encrypt.Key(Uint8List(32)).base64 + encrypt.IV(ivBytes).base64;
-}
-
-/// Salsa20 has no MAC, so a wrong key decrypts to garbage. The first two bytes
-/// are `{"` about once in 2^16 tries; keep going until that happens, then skip
-/// the much rarer case where the garbage is also valid JSON (that would rewrite).
-String _findWrongPasswordWithJsonObjectPrefix(String ciphertext) {
- const maxAttempts = 1 << 20;
- for (var nonce = 0; nonce < maxAttempts; nonce++) {
- final candidate = _passwordFromNonce(nonce);
- final bytes = _salsa20DecryptBytes(candidate, ciphertext);
- if (bytes.length < 2 || bytes[0] != 0x7b || bytes[1] != 0x22) {
- continue;
- }
- try {
- json.decode(_salsa20Decrypt(candidate, ciphertext));
- } catch (_) {
- return candidate;
- }
- }
- fail('no Salsa20 {" prefix collision in $maxAttempts attempts');
-}
diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart
index 1c71ce11..f428b354 100644
--- a/lib/view_model/dashboard/dashboard_view_model.dart
+++ b/lib/view_model/dashboard/dashboard_view_model.dart
@@ -55,7 +55,7 @@ import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_history.dart';
import 'package:cw_core/transaction_info.dart';
-import 'package:cw_core/encryption_file_utils.dart';
+import 'package:cw_core/utils/file.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
@@ -1515,8 +1515,7 @@ abstract class DashboardViewModelBase with Store {
if (walletInfo.type == WalletType.bitcoin) {
final password = await keyService.getWalletPassword(walletName: walletInfo.name);
final path = await pathForWallet(name: walletInfo.name, type: walletInfo.type);
- final encryption = encryptionFileUtilsFor(SettingsStoreBase.walletPasswordDirectInput);
- final jsonSource = await encryption.read(path: path, password: password);
+ final jsonSource = await read(path: path, password: password);
final data = json.decode(jsonSource) as Map;
final mnemonic = data['mnemonic'] as String?;
diff --git a/pubspec.lock b/pubspec.lock
index 4cd50bf7..f37ff20e 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -388,8 +388,8 @@ packages:
dependency: "direct main"
description:
path: "."
- ref: b5d86a2a21e1c186cd0baa4df1f3a4f3f9413056
- resolved-ref: b5d86a2a21e1c186cd0baa4df1f3a4f3f9413056
+ ref: "3aba867dcab6737f6707782f5db15d71f303db38"
+ resolved-ref: "3aba867dcab6737f6707782f5db15d71f303db38"
url: "https://github.com/cake-tech/cake_backup.git"
source: git
version: "1.0.0+1"
diff --git a/pubspec_base.yaml b/pubspec_base.yaml
index 52a41ad6..e8c75f95 100644
--- a/pubspec_base.yaml
+++ b/pubspec_base.yaml
@@ -85,7 +85,7 @@ dependencies:
cake_backup:
git:
url: https://github.com/cake-tech/cake_backup.git
- ref: b5d86a2a21e1c186cd0baa4df1f3a4f3f9413056
+ ref: 3aba867dcab6737f6707782f5db15d71f303db38
flutter_plugin_android_lifecycle: 2.0.23
path_provider_android: ^2.2.1
shared_preferences_android: ^2.4.8
@@ -247,7 +247,7 @@ dev_dependencies:
# cake_backup:
# git:
# url: https://github.com/cake-tech/cake_backup.git
-# ref: b5d86a2a21e1c186cd0baa4df1f3a4f3f9413056
+# ref: 3aba867dcab6737f6707782f5db15d71f303db38
flutter_icons:
image_path: "assets/images/app_logo.png"
diff --git a/pubspec_overrides.yaml b/pubspec_overrides.yaml
index 2bf9d2ce..15910a4f 100644
--- a/pubspec_overrides.yaml
+++ b/pubspec_overrides.yaml
@@ -174,7 +174,7 @@ dependency_overrides:
cake_backup:
git:
url: https://github.com/cake-tech/cake_backup.git
- ref: b5d86a2a21e1c186cd0baa4df1f3a4f3f9413056
+ ref: 3aba867dcab6737f6707782f5db15d71f303db38
characters:
git:
url: https://github.com/dart-lang/core
Why this scored 59/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.