Skip to content

Commit 0e4bf66

Browse files
authored
Merge pull request #2557 from SatoshiPortal/audit-security-deps
refactor: Security audit fixes (certs, webview, RNG) + drop 5 dependencies
2 parents d6f5d4f + ae7719e commit 0e4bf66

25 files changed

Lines changed: 699 additions & 220 deletions

assets/icons/github.svg

Lines changed: 1 addition & 0 deletions
Loading

lib/core/electrum/adapters/drift_electrum_transaction_repository.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:bb_mobile/core/electrum/domain/repositories/electrum_transaction_repository.dart';
2+
import 'package:bb_mobile/core/electrum/domain/value_objects/electrum_connection.dart';
23
import 'package:bb_mobile/core/electrum/frameworks/drift/datasources/electrum_remote_datasource.dart';
34
import 'package:bb_mobile/core/storage/tables/transactions_table.dart';
45
import 'package:bb_mobile/core/utils/bitcoin_tx.dart';
@@ -11,10 +12,10 @@ class DriftElectrumTransactionRepository
1112

1213
@override
1314
Future<BitcoinTx> fetch({
14-
required String serverUrl,
15+
required ElectrumConnection connection,
1516
required String txid,
1617
}) async {
17-
final model = await _datasource.fetch(serverUrl: serverUrl, txid: txid);
18+
final model = await _datasource.fetch(connection: connection, txid: txid);
1819
return TransactionModelExtension.toEntity(model);
1920
}
2021
}

lib/core/electrum/adapters/electrum_transaction_port_adapter.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ class ElectrumTransactionPortAdapter implements TransactionPort {
3737
try {
3838
return await _serversPort.runWithFallback(
3939
network: network,
40-
operation: (server) async {
40+
operation: (connection) async {
4141
final bitcoinTx = await _repository.fetch(
42-
serverUrl: server.url,
42+
connection: connection,
4343
txid: txid,
4444
);
4545
return TransactionMapper.fromBitcoinTx(

lib/core/electrum/adapters/server_status_adapter.dart

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ class ServerStatusAdapter implements ServerStatusPort {
5858
Future<ElectrumServerStatus> checkElectrum({
5959
required String url,
6060
required ElectrumServerNetwork network,
61+
required bool validateDomain,
6162
int? timeout,
6263
}) async {
6364
try {
@@ -79,6 +80,7 @@ class ServerStatusAdapter implements ServerStatusPort {
7980
uri: uri,
8081
request: request,
8182
timeoutSeconds: effectiveTimeout,
83+
validateDomain: validateDomain,
8284
);
8385

8486
if (response.isEmpty) return ElectrumServerStatus.offline;
@@ -135,13 +137,17 @@ class ServerStatusAdapter implements ServerStatusPort {
135137
required Uri uri,
136138
required String request,
137139
required int timeoutSeconds,
140+
required bool validateDomain,
138141
}) async {
142+
// A null onBadCertificate callback enforces strict CA validation. The
143+
// flag comes from the user's electrum settings, so the probe accepts
144+
// exactly the certificates the BDK/LWK sync would accept.
139145
final Socket socket = uri.scheme == 'ssl'
140146
? await SecureSocket.connect(
141147
uri.host,
142148
uri.port,
143149
timeout: Duration(seconds: timeoutSeconds),
144-
onBadCertificate: (_) => true, // accept self-signed certs
150+
onBadCertificate: validateDomain ? null : (_) => true,
145151
)
146152
: await Socket.connect(
147153
uri.host,

lib/core/electrum/application/usecases/add_custom_server_usecase.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import 'package:bb_mobile/core/electrum/application/dtos/requests/add_custom_server_request.dart';
22
import 'package:bb_mobile/core/electrum/domain/entities/electrum_server.dart';
3+
import 'package:bb_mobile/core/electrum/domain/entities/electrum_settings.dart';
34
import 'package:bb_mobile/core/electrum/domain/errors/electrum_failure.dart';
45
import 'package:bb_mobile/core/electrum/domain/ports/server_status_port.dart';
56
import 'package:bb_mobile/core/electrum/domain/repositories/electrum_server_repository.dart';
7+
import 'package:bb_mobile/core/electrum/domain/repositories/electrum_settings_repository.dart';
68
import 'package:bb_mobile/core/electrum/domain/value_objects/electrum_server_network.dart';
79
import 'package:bb_mobile/core/electrum/domain/value_objects/electrum_server_status.dart';
810
import 'package:bb_mobile/core/settings/domain/repositories/settings_repository.dart';
@@ -12,11 +14,13 @@ import 'package:meta/meta.dart';
1214

1315
class AddCustomServerUsecase {
1416
final ElectrumServerRepository _electrumServerRepository;
17+
final ElectrumSettingsRepository _electrumSettingsRepository;
1518
final ServerStatusPort _serverStatusPort;
1619
final SettingsRepository _settingsRepository;
1720

1821
AddCustomServerUsecase({
1922
required this._electrumServerRepository,
23+
required this._electrumSettingsRepository,
2024
required this._serverStatusPort,
2125
required this._settingsRepository,
2226
});
@@ -61,9 +65,23 @@ class AddCustomServerUsecase {
6165

6266
// Step 2: verify the server actually serves chain data by fetching a
6367
// known historical tx (falls back to server.version on testnets).
68+
// Probe with the user's own validateDomain setting: accepting a
69+
// certificate the sync would refuse saves a server that can never be
70+
// used, and the failure only surfaces later as a broken sync.
71+
final ElectrumSettings electrumSettings;
72+
switch (await _electrumSettingsRepository.fetchByNetwork(
73+
server.network,
74+
)) {
75+
case Ok(:final value):
76+
electrumSettings = value;
77+
case Err(:final failure):
78+
return Err(failure);
79+
}
80+
6481
final protocolStatus = await _serverStatusPort.checkElectrum(
6582
url: server.url,
6683
network: server.network,
84+
validateDomain: electrumSettings.validateDomain,
6785
);
6886
if (protocolStatus == ElectrumServerStatus.offline) {
6987
return const Err(ElectrumServerUnreachableFailure());

lib/core/electrum/domain/ports/server_status_port.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,20 @@ abstract class ServerStatusPort {
1414
/// only responds to `server.version` can still be desynced, pruned, or
1515
/// otherwise broken — fetching a real tx proves it can answer wallet
1616
/// queries. Falls back to `server.version` on testnets (no stable txid).
17+
///
18+
/// [validateDomain] must mirror the user's electrum setting of the same
19+
/// name — the flag the BDK/LWK sync obeys. When true, the certificate
20+
/// chain, expiry and hostname must all check out; when false, every check
21+
/// is skipped, so an active MITM becomes indistinguishable from the user's
22+
/// own node and the user vouches for the endpoint.
23+
///
24+
/// Probing with anything else makes this check lie: a laxer probe reports
25+
/// a server online that the sync will then refuse, and a stricter one hides
26+
/// a server that would have worked.
1727
Future<ElectrumServerStatus> checkElectrum({
1828
required String url,
1929
required ElectrumServerNetwork network,
30+
required bool validateDomain,
2031
int? timeout,
2132
});
2233
}
Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
1+
import 'package:bb_mobile/core/electrum/domain/value_objects/electrum_connection.dart';
12
import 'package:bb_mobile/core/utils/bitcoin_tx.dart';
23

34
/// Fetches a parsed Bitcoin transaction from a given Electrum server.
45
///
56
/// Server selection and fallback are the caller's responsibility — this repo
6-
/// is a thin abstraction over a single server fetch (with local caching).
7+
/// is a thin abstraction over a single server fetch (with local caching), and
8+
/// takes the already-resolved [ElectrumConnection] so the transport (scheme,
9+
/// certificate validation, timeout) matches what the sync path uses.
710
/// Returns the parsed [BitcoinTx]; cross-module domain mapping is left to
811
/// callers so the electrum module's domain does not depend on another
912
/// module's domain entities.
1013
abstract class ElectrumTransactionRepository {
11-
Future<BitcoinTx> fetch({required String serverUrl, required String txid});
14+
Future<BitcoinTx> fetch({
15+
required ElectrumConnection connection,
16+
required String txid,
17+
});
1218
}

lib/core/electrum/frameworks/di/electrum_locator.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ class ElectrumLocator {
8888
locator.registerFactory<AddCustomServerUsecase>(
8989
() => AddCustomServerUsecase(
9090
electrumServerRepository: locator<ElectrumServerRepository>(),
91+
electrumSettingsRepository: locator<ElectrumSettingsRepository>(),
9192
serverStatusPort: locator<ServerStatusPort>(),
9293
settingsRepository: locator<SettingsRepository>(),
9394
),

lib/core/electrum/frameworks/drift/datasources/electrum_remote_datasource.dart

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'dart:convert';
2-
import 'dart:io' show SecureSocket;
2+
import 'dart:io' show SecureSocket, Socket;
33

4+
import 'package:bb_mobile/core/electrum/domain/value_objects/electrum_connection.dart';
45
import 'package:bb_mobile/core/storage/sqlite_database.dart';
56
import 'package:bb_mobile/core/utils/bitcoin_tx.dart';
67
import 'package:convert/convert.dart';
@@ -11,7 +12,7 @@ class ElectrumRemoteDatasource {
1112
ElectrumRemoteDatasource({required this._sqlite});
1213

1314
Future<TransactionModel> fetch({
14-
required String serverUrl,
15+
required ElectrumConnection connection,
1516
required String txid,
1617
}) async {
1718
final cachedTransaction = await _sqlite.managers.transactions
@@ -20,7 +21,7 @@ class ElectrumRemoteDatasource {
2021

2122
if (cachedTransaction != null) return cachedTransaction;
2223

23-
final txBytes = await _getTransaction(Uri.parse(serverUrl), txid);
24+
final txBytes = await _getTransaction(connection, txid);
2425
final tx = await BitcoinTx.fromBytes(txBytes);
2526

2627
final txModel = TransactionModel(
@@ -37,9 +38,12 @@ class ElectrumRemoteDatasource {
3738
return txModel;
3839
}
3940

40-
Future<List<int>> _getTransaction(Uri serverUri, String txid) async {
41+
Future<List<int>> _getTransaction(
42+
ElectrumConnection connection,
43+
String txid,
44+
) async {
4145
try {
42-
final socket = await SecureSocket.connect(serverUri.host, serverUri.port);
46+
final socket = await _connect(connection);
4347

4448
final request = {
4549
'id': 1,
@@ -58,4 +62,36 @@ class ElectrumRemoteDatasource {
5862
throw Exception('Electrum RPC error: $e');
5963
}
6064
}
65+
66+
/// Opens the socket described by the resolved [connection] rather than
67+
/// assuming a CA-validated TLS endpoint.
68+
///
69+
/// A `tcp://` server is reached in the clear, and certificates are validated
70+
/// according to the user's `validateDomain` setting — the very flag the
71+
/// BDK/LWK sync obeys — so a personal node with a self-signed certificate
72+
/// behaves the same on both paths instead of syncing fine but failing here.
73+
Future<Socket> _connect(ElectrumConnection connection) {
74+
final uri = _parseUrl(connection.url);
75+
final timeout = Duration(seconds: connection.timeout);
76+
77+
if (uri.scheme == 'tcp') {
78+
return Socket.connect(uri.host, uri.port, timeout: timeout);
79+
}
80+
81+
return SecureSocket.connect(
82+
uri.host,
83+
uri.port,
84+
timeout: timeout,
85+
onBadCertificate: connection.validateDomain ? null : (_) => true,
86+
);
87+
}
88+
89+
/// Servers are stored either with an explicit `ssl://` / `tcp://` scheme or
90+
/// as a bare `host:port` (how Liquid urls are persisted), which `Uri.parse`
91+
/// would otherwise read as the scheme. Bare urls default to TLS, matching
92+
/// the rest of the electrum module.
93+
Uri _parseUrl(String url) =>
94+
url.startsWith('ssl://') || url.startsWith('tcp://')
95+
? Uri.parse(url)
96+
: Uri.parse('ssl://$url');
6197
}

lib/core/exchange/domain/usecases/create_log_attachment_usecase.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class CreateLogAttachmentUsecase {
3030
final logContent = logs.join('\n');
3131
final bytes = Uint8List.fromList(utf8.encode(logContent));
3232

33-
final random = Random();
33+
final random = Random.secure();
3434
final timestamp = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now());
3535

3636
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';

0 commit comments

Comments
 (0)