Revert "fix: unify encryption across platforms (#3470)" (#3634)

This reverts commit 90e25afc11.
This commit is contained in:
Omar Hatem
2026-09-18 00:33:16 +02:00
committed by GitHub
parent 3a04c7ff1e
commit dd58455ea6
10 changed files with 88 additions and 557 deletions
+27 -20
View File
@@ -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});
final bool 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}) {
return file.write(
path: path,
password: password,
data: data,
highEntropyPassphrase: !isDirect,
);
Future<void> write(
{required String path, required String password, required String data}) async =>
await file.write(path: path, password: password, data: data);
// 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}) 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);
}
}
+27
View File
@@ -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;
}
+13 -128
View File
@@ -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}) async =>
writeData(path: path, password: password, data: data);
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<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> 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;
}
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<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);
}
+12 -20
View File
@@ -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? ?? "";
+2 -1
View File
@@ -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
@@ -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');
}
@@ -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?;
+2 -2
View File
@@ -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"
+2 -2
View File
@@ -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"
+1 -1
View File
@@ -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