Skip to content

Commit 1ea7444

Browse files
committed
feat(recoverbull): isolate key-server Tor sessions
1 parent 7b213da commit 1ea7444

39 files changed

Lines changed: 1280 additions & 381 deletions

FEATURES.md

Lines changed: 10 additions & 6 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]
@@ -64,6 +64,7 @@ graph TB
6464
ALL_SEED_VIEW --> APP_UNLOCK
6565
ANNOUNCEMENTS --> SETTINGS
6666
APP_STARTUP --> WALLETS
67+
APP_STARTUP --> TOR
6768
AUTOSWAPS --> TRANSFER
6869
BIP85 --> SECRETS
6970
BIP85 --> SETTINGS
@@ -105,9 +106,10 @@ graph TB
105106
SETTINGS --> CORE
106107
SETTINGS --> BULL_PAYJOIN
107108
STATUS --> BULL_PAYJOIN
109+
STATUS --> TOR
108110
SWAPS --> BULL_PAYJOIN
109111
SWAPS --> UTXO_MGMT
110-
TOR --> CORE
112+
CORE --> TOR
111113
TRANSFER --> CONSOLIDATION
112114
TRANSFER --> SEND
113115
TRANSFER --> RECEIVE
@@ -129,8 +131,8 @@ graph TB
129131
classDef featureStyle fill:#1a202c,stroke:#2d3748,stroke-width:2px,color:#e2e8f0
130132
131133
class CORE coreStyle
132-
class PRIMITIVES,BULL_PAYJOIN packageStyle
133-
class SETTINGS,TOR,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
134+
class PRIMITIVES,BULL_PAYJOIN,TOR packageStyle
135+
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
134136
```
135137

136138
## About Package Dependency Diagrams
@@ -169,7 +171,8 @@ graph TB
169171
- Database (Drift/SQLite)
170172
- Secure Storage instance (Flutter Secure Storage)
171173
- API Clients (REST/GraphQL clients)
172-
- Factory for a HTTP client to connect to Tor
174+
- Embedded Onion adapter with isolated RecoverBull and Bitcoin Electrum `.onion` sessions
175+
- Explicit Orbot SOCKS override for Bitcoin Electrum `.onion` servers
173176
- UI Kit (shared widgets, theme)
174177
- DI setup and interfaces (Service Locator pattern)
175178
- Router setup and interfaces (Navigation)
@@ -188,6 +191,7 @@ graph TB
188191
### Central Features (Highly Depended Upon)
189192

190193
- **Core**: Foundation for all features
194+
- **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
191195
- **Wallets**: Used by Send, UTXO Management, Transaction History, Backups, App Startup
192196
- **Secrets**: Used by Wallets, BIP85
193197
- **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);
@@ -48,6 +58,7 @@ class CoreLocator {
4858
}
4959

5060
static Future<void> registerRepositories(GetIt locator) async {
61+
bull_tor.TorLocator.registerRepositories(locator);
5162
await TorLocator.registerRepositories(locator);
5263
BlockchainLocator.registerRepositories(locator);
5364
ElectrumLocator.registerRepositories(locator);
@@ -56,7 +67,7 @@ class CoreLocator {
5667
MempoolLocator.registerRepositories(locator);
5768
await SettingsLocator.registerRepositories(locator);
5869
SeedLocator.registerRepositories(locator);
59-
await RecoverbullLocator.registerRepositories(locator);
70+
RecoverbullLocator.registerRepositories(locator);
6071
SwapsLocator.registerRepositories(locator);
6172
WalletLocator.registerRepositories(locator);
6273
Bip85DerivationsLocator.registerRepositories(locator);
@@ -72,6 +83,7 @@ class CoreLocator {
7283
}
7384

7485
static void registerUsecases(GetIt locator) {
86+
bull_tor.TorLocator.registerUsecases(locator);
7587
LabelsLocator.registerUseCases(locator);
7688
ElectrumLocator.registerUsecases(locator);
7789
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
@@ -73,15 +71,15 @@ class RecoverBullRepository {
7371
String password,
7472
String salt,
7573
String vaultKey,
74+
TorProxyEndpoint endpoint,
7675
) async {
7776
try {
78-
final externalProxy = await torConfigPort.getAvailableExternalTorConfig();
7977
await remoteDatasource.store(
8078
convert.hex.decode(_normalizeHex(identifier)),
8179
utf8.encode(password),
8280
convert.hex.decode(_normalizeHex(salt)),
8381
convert.hex.decode(_normalizeHex(vaultKey)),
84-
externalProxy: externalProxy,
82+
endpoint: endpoint,
8583
);
8684
return const Ok(null);
8785
} on recoverbull.KeyServerException catch (e, st) {
@@ -97,14 +95,14 @@ class RecoverBullRepository {
9795
String identifier,
9896
String password,
9997
String salt,
98+
TorProxyEndpoint endpoint,
10099
) async {
101100
try {
102-
final externalProxy = await torConfigPort.getAvailableExternalTorConfig();
103101
final vaultKey = await remoteDatasource.fetch(
104102
convert.hex.decode(_normalizeHex(identifier)),
105103
utf8.encode(password),
106104
convert.hex.decode(_normalizeHex(salt)),
107-
externalProxy: externalProxy,
105+
endpoint: endpoint,
108106
);
109107
return Ok(convert.hex.encode(vaultKey));
110108
} on recoverbull.KeyServerException catch (e, st) {
@@ -120,23 +118,22 @@ class RecoverBullRepository {
120118
String identifier,
121119
String password,
122120
String salt,
121+
TorProxyEndpoint endpoint,
123122
) async {
124-
final externalProxy = await torConfigPort.getAvailableExternalTorConfig();
125123
await remoteDatasource.trash(
126124
convert.hex.decode(_normalizeHex(identifier)),
127125
utf8.encode(password),
128126
convert.hex.decode(_normalizeHex(salt)),
129-
externalProxy: externalProxy,
127+
endpoint: endpoint,
130128
);
131129
}
132130

133131
/// Health probe: completes normally when the server is reachable, throws
134132
/// otherwise. Kept throwing (not Result) on purpose so the shared status
135133
/// checker — `CheckServerConnectionUsecase`, which turns the throw/return
136134
/// into the bool — is unaffected by the Result migration.
137-
Future<void> checkConnection() async {
138-
final externalProxy = await torConfigPort.getAvailableExternalTorConfig();
139-
await remoteDatasource.checkConnection(externalProxy: externalProxy);
135+
Future<void> checkConnection(TorProxyEndpoint endpoint) async {
136+
await remoteDatasource.checkConnection(endpoint);
140137
}
141138

142139
Future<Uri> fetchUrl() async {

0 commit comments

Comments
 (0)