Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 18 additions & 25 deletions cw_core/lib/encryption_file_utils.dart
Original file line number Diff line number Diff line change
@@ -1,41 +1,34 @@
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 direct) =>
direct ? XChaCha20EncryptionFileUtils() : Salsa20EncryhptionFileUtils();
EncryptionFileUtils encryptionFileUtilsFor(bool isDirect) =>
XChaCha20MigratingEncryptionFileUtils(isDirect: isDirect);

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 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);
class XChaCha20MigratingEncryptionFileUtils extends EncryptionFileUtils {
XChaCha20MigratingEncryptionFileUtils({required this.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);
}
final bool isDirect;

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);
Future<void> write({required String path, required String password, required String data}) {
return file.write(
path: path,
password: password,
data: data,
highEntropyPassphrase: !isDirect,
);
}

@override
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);
Future<String> read({required String path, required String password}) {
return file.read(
path: path,
password: password,
highEntropyPassphrase: !isDirect,
);
}
}
27 changes: 0 additions & 27 deletions cw_core/lib/key.dart
Original file line number Diff line number Diff line change
@@ -1,35 +1,8 @@
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;
}
141 changes: 128 additions & 13 deletions cw_core/lib/utils/file.dart
Original file line number Diff line number Diff line change
@@ -1,21 +1,84 @@
import 'dart:convert';
import 'dart:io';
import 'package:cw_core/key.dart';
import 'dart:typed_data';

import 'package:cake_backup/backup.dart' as cwb;
import 'package:encrypt/encrypt.dart' as encrypt;

Future<void> write({required String path, required String password, required String data}) async =>
writeData(path: path, password: password, data: data);
const _ivEncodedStringLength = 12;

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<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}) async {
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,
);
Comment on lines +60 to +65

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should wrap this in a try/catch, so a failure here won't stop wallet from opening/read from completing

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it shouldn't fail at all and if it does it better do so loudly

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it will not fail loudly, it will just fail to open the wallet silently, and the user will end up with a wallet that can't be opened, and since it passed our tests, so we don't need it to fail loudly, we need it to work, and for any reason the user might have, we shouldn't block him from opening his wallet, we can add another way to check if the migration worked tho, but it will be a bit of a headache

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, but it can only fail if

  1. secure random is unavailable (impossible)
  2. file wasn't written (out of space / wrong permissions)

and even if it fails a fix is a matter of reopening the app after solving the problem. Unless I'm missing something

}

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 {
final file = File(path);

if (!file.existsSync()) {
Expand All @@ -24,5 +87,57 @@ Future<String> read({required String path, required String password}) async {

final encrypted = file.readAsStringSync();

return decode(password: password, data: encrypted);
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);
}
32 changes: 20 additions & 12 deletions cw_core/lib/wallet_service.dart
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import "dart:convert";
import "dart:io";

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;
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;

abstract class WalletService<N extends WalletCredentials, RFS extends WalletCredentials,
RFK extends WalletCredentials, RFH extends WalletCredentials> {
Expand Down Expand Up @@ -95,8 +96,15 @@ 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 read(path: path, password: password);
final jsonSource = await encryption.read(path: path, password: password);
try {
final data = json.decode(jsonSource) as Map;
return data["mnemonic"] as String? ?? "";
Expand Down
3 changes: 1 addition & 2 deletions cw_core/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ dependencies:
cake_backup:
git:
url: https://github.com/cake-tech/cake_backup.git
ref: main
version: 1.0.0
ref: b5d86a2a21e1c186cd0baa4df1f3a4f3f9413056
socks5_proxy:
git:
url: https://github.com/LacticWhale/socks_dart
Expand Down
Loading
Loading