Skip to content

Commit 5e1b34c

Browse files
committed
feat(recoverbull): isolate key-server Tor sessions
1 parent 999f662 commit 5e1b34c

39 files changed

Lines changed: 1325 additions & 378 deletions

FEATURES.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ This diagram shows the dependencies between features in the Bull Bitcoin Mobile
99
```mermaid
1010
graph TB
1111
%% Core infrastructure
12-
CORE[Core<br/>---<br/>Database, Secure Storage,<br/>API Clients, Tor HTTP Client, UI Kit,<br/>DI & Router setup,<br/>PIN encrypted storage,<br/>Domain Primitives/Value Objects]
12+
CORE[Core<br/>---<br/>Database, Secure Storage,<br/>API Clients, Tor Adapters, UI Kit,<br/>DI & Router setup,<br/>PIN encrypted storage,<br/>Domain Primitives/Value Objects]
1313
PRIMITIVES[Primitives Package]
1414
BULL_PAYJOIN[Bull Payjoin Package<br/>Public contract]
1515
1616
%% Feature modules
1717
SETTINGS[Settings]
18-
TOR[Tor]
18+
TOR[Tor<br/>Workspace Package]
1919
PIN_CODE[Pin Code]
2020
LABELS[Labels]
2121
SECRETS[Secrets]
@@ -66,6 +66,8 @@ graph TB
6666
ANNOUNCEMENTS --> SWAPS
6767
APP_STARTUP --> WALLETS
6868
AUTOSWAP --> SWAPS
69+
APP_STARTUP --> TOR
70+
AUTOSWAPS --> TRANSFER
6971
BIP85 --> SECRETS
7072
BIP85 --> SETTINGS
7173
BACKUPS --> BIP85
@@ -108,11 +110,12 @@ graph TB
108110
SETTINGS --> CORE
109111
SETTINGS --> BULL_PAYJOIN
110112
STATUS --> BULL_PAYJOIN
113+
STATUS --> TOR
111114
SWAPS --> BULL_PAYJOIN
112115
SWAPS --> EXCHANGE
113116
SWAPS --> LABELS
114117
SWAPS --> UTXO_MGMT
115-
TOR --> CORE
118+
CORE --> TOR
116119
TRANSFER --> CONSOLIDATION
117120
TRANSFER --> SEND
118121
TRANSFER --> RECEIVE
@@ -136,7 +139,8 @@ graph TB
136139
classDef featureStyle fill:#1a202c,stroke:#2d3748,stroke-width:2px,color:#e2e8f0
137140
138141
class CORE coreStyle
139-
class PRIMITIVES,BULL_PAYJOIN packageStyle
142+
class PRIMITIVES,BULL_PAYJOIN,TOR packageStyle
143+
class SETTINGS,PIN_CODE,LABELS,SECRETS,HW_WALLETS,BTC_PRICE,NETWORK,BIP85,FEES,WALLETS,EXCHANGE,APP_STARTUP,UTXO_MGMT,ADDRESS_MGMT,RECIPIENTS,FUNDING,BACKUPS,SWAPS,WITHDRAWAL,STATUS,SEND,RECEIVE,TRANSFER,TX_HISTORY,BG_TASKS,AUTOSWAPS,DCA,SELL,PAY,BUY,COINS,ANNOUNCEMENTS,CONSOLIDATION,ALL_SEED_VIEW,APP_UNLOCK featureStyle
140144
```
141145

142146
## About Package Dependency Diagrams
@@ -175,7 +179,8 @@ graph TB
175179
- Database (Drift/SQLite)
176180
- Secure Storage instance (Flutter Secure Storage)
177181
- API Clients (REST/GraphQL clients)
178-
- Factory for a HTTP client to connect to Tor
182+
- Embedded Onion adapter with isolated RecoverBull and Bitcoin Electrum `.onion` sessions
183+
- Explicit Orbot SOCKS override for Bitcoin Electrum `.onion` servers
179184
- UI Kit (shared widgets, theme)
180185
- DI setup and interfaces (Service Locator pattern)
181186
- Router setup and interfaces (Navigation)
@@ -194,6 +199,7 @@ graph TB
194199
### Central Features (Highly Depended Upon)
195200

196201
- **Core**: Foundation for all features
202+
- **Tor**: `packages/tor` — embedded Onion lifecycle with isolated RecoverBull and Bitcoin Electrum `.onion` sessions, plus external SOCKS verification for the explicit Electrum Orbot override. Depends on Flutter: it owns a platform plugin and app-directory storage, which is the infrastructure-package exception in AGENTS.md
197203
- **Wallets**: Used by Send, UTXO Management, Transaction History, Backups, App Startup
198204
- **Secrets**: Used by Wallets, BIP85
199205
- **Settings**: Used by Wallets, Exchange, BIP85, Bitcoin Price

integration_test/recoverbull_test.dart

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import 'package:bb_mobile/core/recoverbull/domain/usecases/restore_vault_usecase
77
import 'package:bb_mobile/core/seed/data/models/seed_model.dart';
88
import 'package:bb_mobile/core/seed/data/repository/seed_repository.dart';
99
import 'package:bb_mobile/core/settings/domain/settings_entity.dart';
10-
import 'package:bb_mobile/core/tor/data/usecases/init_tor_usecase.dart';
1110
import 'package:bb_mobile/core/utils/bip32_derivation.dart';
1211
import 'package:bb_mobile/core/utils/result.dart';
1312
import 'package:bb_mobile/core/utils/recoverbull_bip85.dart';
@@ -18,12 +17,13 @@ import 'package:bb_mobile/main.dart';
1817
import 'package:bip39_mnemonic/bip39_mnemonic.dart' as bip39;
1918
import 'package:flutter/foundation.dart';
2019
import 'package:flutter_test/flutter_test.dart';
20+
import 'package:bull_tor/tor.dart';
2121

2222
Future<void> main({bool isInitialized = false}) async {
2323
TestWidgetsFlutterBinding.ensureInitialized();
2424
if (!isInitialized) await Bull.init();
2525

26-
final initializeTorUsecase = locator<InitTorUsecase>();
26+
final ensureTorReadyUsecase = locator<EnsureTorReadyUsecase>();
2727
final restoreVaultUsecase = locator<RestoreVaultUsecase>();
2828
final decryptVaultUsecase = locator<DecryptVaultUsecase>();
2929
final fetchVaultKeyFromServerUsecase =
@@ -65,7 +65,10 @@ Future<void> main({bool isInitialized = false}) async {
6565
Network.bitcoinMainnet,
6666
);
6767

68-
setUpAll(() async => await initializeTorUsecase.execute());
68+
setUpAll(() async {
69+
final state = await ensureTorReadyUsecase.execute();
70+
expect(state, isA<TorReady>());
71+
});
6972

7073
group('Recoverbull', () {
7174
// Fetches the vault key over Tor from the RecoverBull key server. Tor can

lib/core/core_locator.dart

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ import 'package:bb_mobile/core/storage/sqlite_database.dart';
1414
import 'package:bb_mobile/core/storage/storage_locator.dart';
1515
import 'package:bb_mobile/core/swaps/swaps_locator.dart';
1616
import 'package:bb_mobile/core/tor/tor_locator.dart';
17+
import 'package:bb_mobile/core/utils/logger.dart';
1718
import 'package:bb_mobile/core/wallet/wallet_locator.dart';
19+
import 'package:bull_tor/tor_adapter.dart' as bull_tor;
1820
import 'package:get_it/get_it.dart';
1921

2022
class CoreLocator {
@@ -23,13 +25,21 @@ class CoreLocator {
2325
}
2426

2527
static Future<void> registerDatasources(GetIt locator) async {
28+
await bull_tor.TorLocator.registerDatasources(
29+
locator,
30+
logger: bull_tor.TorLogger(
31+
configCallback: log.config,
32+
fineCallback: log.fine,
33+
warningCallback: log.warning,
34+
),
35+
);
2636
await TorLocator.registerDatasources(locator);
2737
BlockchainLocator.registerDatasources(locator);
2838
await ElectrumLocator.registerDatasources(locator);
2939
ExchangeLocator.registerDatasources(locator);
3040
FeesLocator.registerDatasources(locator);
3141
await MempoolLocator.registerDatasources(locator);
32-
await RecoverbullLocator.registerDatasources(locator);
42+
RecoverbullLocator.registerDatasources(locator);
3343
await StorageLocator.registerDatasources(locator);
3444
SeedLocator.registerDatasources(locator);
3545
await SwapsLocator.registerDatasources(locator);
@@ -47,6 +57,7 @@ class CoreLocator {
4757
}
4858

4959
static Future<void> registerRepositories(GetIt locator) async {
60+
bull_tor.TorLocator.registerRepositories(locator);
5061
await TorLocator.registerRepositories(locator);
5162
BlockchainLocator.registerRepositories(locator);
5263
ElectrumLocator.registerRepositories(locator);
@@ -55,7 +66,7 @@ class CoreLocator {
5566
MempoolLocator.registerRepositories(locator);
5667
await SettingsLocator.registerRepositories(locator);
5768
SeedLocator.registerRepositories(locator);
58-
await RecoverbullLocator.registerRepositories(locator);
69+
RecoverbullLocator.registerRepositories(locator);
5970
SwapsLocator.registerRepositories(locator);
6071
WalletLocator.registerRepositories(locator);
6172
Bip85DerivationsLocator.registerRepositories(locator);
@@ -70,6 +81,7 @@ class CoreLocator {
7081
}
7182

7283
static void registerUsecases(GetIt locator) {
84+
bull_tor.TorLocator.registerUsecases(locator);
7385
LabelsLocator.registerUseCases(locator);
7486
ElectrumLocator.registerUsecases(locator);
7587
BlockchainLocator.registerUsecases(locator);
Lines changed: 23 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,28 @@
11
import 'package:bb_mobile/core/recoverbull/data/datasources/recoverbull_settings_datasource.dart';
2-
import 'package:bb_mobile/core/tor/data/datasources/tor_datasource.dart';
3-
import 'package:bb_mobile/core/tor/domain/value_objects/tor_proxy_config.dart';
4-
import 'package:bb_mobile/core/tor/tor_status.dart';
52
import 'package:bb_mobile/core/utils/logger.dart';
63
import 'package:recoverbull/recoverbull.dart';
4+
import 'package:bull_tor/tor.dart';
75

86
class RecoverBullRemoteDatasource {
97
final RecoverbullSettingsDatasource _recoverbullSettingsDatasource;
10-
final TorDatasource _torDatasource;
8+
final TorHttpClientFactory _torHttpClientFactory;
119

1210
RecoverBullRemoteDatasource({
1311
required this._recoverbullSettingsDatasource,
14-
required this._torDatasource,
12+
required this._torHttpClientFactory,
1513
});
1614

17-
Future<void> info({TorProxyConfig? externalProxy}) async {
18-
final client = _torDatasource.httpClient(externalProxy: externalProxy);
15+
Future<void> info(TorProxyEndpoint endpoint) async {
16+
final client = _torHttpClientFactory.create(endpoint);
1917
final url = await _recoverbullSettingsDatasource.fetch();
2018
try {
2119
final info = await KeyServer(address: url, client: client).infos();
2220
log.info('KeyServer canary: ${info.canary}');
2321
} catch (e) {
2422
log.severe(error: e, trace: StackTrace.current);
2523
rethrow;
24+
} finally {
25+
client.close(force: true);
2626
}
2727
}
2828

@@ -31,10 +31,10 @@ class RecoverBullRemoteDatasource {
3131
List<int> password,
3232
List<int> salt,
3333
List<int> backupKey, {
34-
TorProxyConfig? externalProxy,
34+
required TorProxyEndpoint endpoint,
3535
}) async {
36+
final client = _torHttpClientFactory.create(endpoint);
3637
try {
37-
final client = _torDatasource.httpClient(externalProxy: externalProxy);
3838
final url = await _recoverbullSettingsDatasource.fetch();
3939
await KeyServer(address: url, client: client).storeBackupKey(
4040
backupId: backupId,
@@ -49,17 +49,19 @@ class RecoverBullRemoteDatasource {
4949
trace: StackTrace.current,
5050
);
5151
rethrow;
52+
} finally {
53+
client.close(force: true);
5254
}
5355
}
5456

5557
Future<List<int>> fetch(
5658
List<int> backupId,
5759
List<int> password,
5860
List<int> salt, {
59-
TorProxyConfig? externalProxy,
61+
required TorProxyEndpoint endpoint,
6062
}) async {
63+
final client = _torHttpClientFactory.create(endpoint);
6164
try {
62-
final client = _torDatasource.httpClient(externalProxy: externalProxy);
6365
final url = await _recoverbullSettingsDatasource.fetch();
6466
return await KeyServer(
6567
address: url,
@@ -72,17 +74,19 @@ class RecoverBullRemoteDatasource {
7274
trace: StackTrace.current,
7375
);
7476
rethrow;
77+
} finally {
78+
client.close(force: true);
7579
}
7680
}
7781

7882
Future<void> trash(
7983
List<int> backupId,
8084
List<int> password,
8185
List<int> salt, {
82-
TorProxyConfig? externalProxy,
86+
required TorProxyEndpoint endpoint,
8387
}) async {
88+
final client = _torHttpClientFactory.create(endpoint);
8489
try {
85-
final client = _torDatasource.httpClient(externalProxy: externalProxy);
8690
final url = await _recoverbullSettingsDatasource.fetch();
8791
await KeyServer(
8892
address: url,
@@ -95,16 +99,14 @@ class RecoverBullRemoteDatasource {
9599
trace: StackTrace.current,
96100
);
97101
rethrow;
102+
} finally {
103+
client.close(force: true);
98104
}
99105
}
100106

101-
Future<void> checkConnection({TorProxyConfig? externalProxy}) async {
107+
Future<void> checkConnection(TorProxyEndpoint endpoint) async {
108+
final client = _torHttpClientFactory.create(endpoint);
102109
try {
103-
if (externalProxy == null) {
104-
await _waitForInternalTor();
105-
}
106-
107-
final client = _torDatasource.httpClient(externalProxy: externalProxy);
108110
final url = await _recoverbullSettingsDatasource.fetch();
109111
await KeyServer(address: url, client: client).infos();
110112
} catch (e) {
@@ -114,19 +116,8 @@ class RecoverBullRemoteDatasource {
114116
trace: StackTrace.current,
115117
);
116118
rethrow;
117-
}
118-
}
119-
120-
Future<void> _waitForInternalTor() async {
121-
const maxWaitTime = Duration(minutes: 2);
122-
final startTime = DateTime.now();
123-
124-
while (_torDatasource.status == TorStatus.connecting) {
125-
if (DateTime.now().difference(startTime) > maxWaitTime) {
126-
throw Exception('Timeout waiting for Tor to be ready');
127-
}
128-
log.info('Waiting for Tor to be ready...');
129-
await Future.delayed(const Duration(seconds: 3));
119+
} finally {
120+
client.close(force: true);
130121
}
131122
}
132123
}

lib/core/recoverbull/data/repository/recoverbull_repository.dart

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,22 @@ import 'package:bb_mobile/core/recoverbull/data/datasources/recoverbull_settings
66
import 'package:bb_mobile/core/recoverbull/domain/entity/decrypted_vault.dart';
77
import 'package:bb_mobile/core/recoverbull/domain/entity/encrypted_vault.dart';
88
import 'package:bb_mobile/core/recoverbull/domain/recoverbull_failure.dart';
9-
import 'package:bb_mobile/core/tor/domain/ports/tor_config_port.dart';
109
import 'package:bb_mobile/core/utils/logger.dart';
1110
import 'package:bb_mobile/core/utils/result.dart';
1211
import 'package:convert/convert.dart' as convert;
1312
import 'package:recoverbull/recoverbull.dart' as recoverbull;
13+
import 'package:bull_tor/tor.dart';
1414

1515
/// Data boundary for the RecoverBull key server and vault crypto. Catches the
1616
/// foreign exceptions the datasources/SDK throw, logs the raw reason, and
1717
/// returns a [RecoverBullCoreFailure] — no exception crosses this boundary.
1818
class RecoverBullRepository {
1919
final RecoverBullRemoteDatasource remoteDatasource;
2020
final RecoverbullSettingsDatasource recoverbullSettingsDatasource;
21-
final TorConfigPort torConfigPort;
2221

2322
RecoverBullRepository({
2423
required this.remoteDatasource,
2524
required this.recoverbullSettingsDatasource,
26-
required this.torConfigPort,
2725
});
2826

2927
/// Builds an encrypted vault file for [plaintext] under [vaultKey] and stamps
@@ -85,15 +83,15 @@ class RecoverBullRepository {
8583
String password,
8684
String salt,
8785
String vaultKey,
86+
TorProxyEndpoint endpoint,
8887
) async {
8988
try {
90-
final externalProxy = await torConfigPort.getAvailableExternalTorConfig();
9189
await remoteDatasource.store(
9290
convert.hex.decode(_normalizeHex(identifier)),
9391
utf8.encode(password),
9492
convert.hex.decode(_normalizeHex(salt)),
9593
convert.hex.decode(_normalizeHex(vaultKey)),
96-
externalProxy: externalProxy,
94+
endpoint: endpoint,
9795
);
9896
return const Ok(null);
9997
} on recoverbull.KeyServerException catch (e, st) {
@@ -119,14 +117,14 @@ class RecoverBullRepository {
119117
String identifier,
120118
String password,
121119
String salt,
120+
TorProxyEndpoint endpoint,
122121
) async {
123122
try {
124-
final externalProxy = await torConfigPort.getAvailableExternalTorConfig();
125123
final vaultKey = await remoteDatasource.fetch(
126124
convert.hex.decode(_normalizeHex(identifier)),
127125
utf8.encode(password),
128126
convert.hex.decode(_normalizeHex(salt)),
129-
externalProxy: externalProxy,
127+
endpoint: endpoint,
130128
);
131129
return Ok(convert.hex.encode(vaultKey));
132130
} on recoverbull.KeyServerException catch (e, st) {
@@ -152,23 +150,22 @@ class RecoverBullRepository {
152150
String identifier,
153151
String password,
154152
String salt,
153+
TorProxyEndpoint endpoint,
155154
) async {
156-
final externalProxy = await torConfigPort.getAvailableExternalTorConfig();
157155
await remoteDatasource.trash(
158156
convert.hex.decode(_normalizeHex(identifier)),
159157
utf8.encode(password),
160158
convert.hex.decode(_normalizeHex(salt)),
161-
externalProxy: externalProxy,
159+
endpoint: endpoint,
162160
);
163161
}
164162

165163
/// Health probe: completes normally when the server is reachable, throws
166164
/// otherwise. Kept throwing (not Result) on purpose so the shared status
167165
/// checker — `CheckServerConnectionUsecase`, which turns the throw/return
168166
/// into the bool — is unaffected by the Result migration.
169-
Future<void> checkConnection() async {
170-
final externalProxy = await torConfigPort.getAvailableExternalTorConfig();
171-
await remoteDatasource.checkConnection(externalProxy: externalProxy);
167+
Future<void> checkConnection(TorProxyEndpoint endpoint) async {
168+
await remoteDatasource.checkConnection(endpoint);
172169
}
173170

174171
Future<Uri> fetchUrl() async {

0 commit comments

Comments
 (0)