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
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import 'package:bb_mobile/core/tor/tor_status.dart';
import 'package:bb_mobile/core/utils/logger.dart';
import 'package:recoverbull/recoverbull.dart';

export 'package:recoverbull/recoverbull.dart'
show AttemptsResult, FetchBackupKeyResult, Info;

class RecoverBullRemoteDatasource {
final RecoverbullSettingsDatasource _recoverbullSettingsDatasource;
final TorDatasource _torDatasource;
Expand All @@ -26,6 +29,112 @@ class RecoverBullRemoteDatasource {
}
}

/// Returns the server info (canary-checked), including the telemetry
/// metadata (`attempts_collection_started_at`, `max_attempt_identifiers`).
Future<Info> infos({
TorProxyConfig? externalProxy,
String? expectedCanary,
}) async {
try {
if (externalProxy == null) {
await _waitForInternalTor();
}
final client = _torDatasource.httpClient(externalProxy: externalProxy);
final url = await _recoverbullSettingsDatasource.fetch();
return await KeyServer(
address: url,
client: client,
).infos(expectedCanary: expectedCanary);
} catch (e) {
log.severe(error: e, trace: StackTrace.current);
rethrow;
}
}

/// Conditional `GET /attempts`: the public brute-force telemetry snapshot.
/// Only the entries matching [backupIds] or [backupIdHashes] are returned;
/// the full snapshot never leaves the client's worker isolate.
Future<AttemptsResult> attempts({
String? etag,
List<List<int>> backupIds = const [],
List<String> backupIdHashes = const [],
TorProxyConfig? externalProxy,
}) async {
try {
if (externalProxy == null) {
await _waitForInternalTor();
}
final client = _torDatasource.httpClient(externalProxy: externalProxy);
final url = await _recoverbullSettingsDatasource.fetch();
return await KeyServer(address: url, client: client).attempts(
etag: etag,
backupIds: backupIds,
backupIdHashes: backupIdHashes,
);
} catch (e) {
log.severe(error: e, trace: StackTrace.current);
rethrow;
}
}

/// Fetch with the exact attempt counters of the identifier's current
/// rate-limit window — the freshest telemetry signal.
Future<FetchBackupKeyResult> fetchWithStatus(
List<int> backupId,
List<int> password,
List<int> salt, {
TorProxyConfig? externalProxy,
}) async {
try {
final client = _torDatasource.httpClient(externalProxy: externalProxy);
final url = await _recoverbullSettingsDatasource.fetch();
return await KeyServer(
address: url,
client: client,
).fetchBackupKeyWithStatus(
backupId: backupId,
password: password,
salt: salt,
);
} catch (e) {
log.severe(
message: 'fetchBackupKeyWithStatus error',
error: e,
trace: StackTrace.current,
);
rethrow;
}
}

/// Trash with the exact attempt counters of the identifier's current
/// rate-limit window.
Future<FetchBackupKeyResult> trashWithStatus(
List<int> backupId,
List<int> password,
List<int> salt, {
TorProxyConfig? externalProxy,
}) async {
try {
final client = _torDatasource.httpClient(externalProxy: externalProxy);
final url = await _recoverbullSettingsDatasource.fetch();
return await KeyServer(
address: url,
client: client,
).trashBackupKeyWithStatus(
backupId: backupId,
password: password,
salt: salt,
);
} catch (e) {
log.severe(
message: 'trashBackupKeyWithStatus error',
error: e,
trace: StackTrace.current,
);
rethrow;
}
}

Future<void> store(
List<int> backupId,
List<int> password,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import 'package:bb_mobile/core/storage/sqlite_database.dart';
import 'package:drift/drift.dart';

/// Persistence for the brute-force telemetry baseline: the `/attempts`
/// polling state per key-server URL, and the user's own operation counters
/// per monitored backup.
///
/// All state is scoped per key-server URL: changing the URL invalidates
/// ETag, baselines and `collectionStartedAt` — identifiers and snapshots
/// from different servers are unrelated.
class RecoverbullTelemetryDatasource {
final SqliteDatabase _sqlite;

RecoverbullTelemetryDatasource({required this._sqlite});

// --- Server polling state (one row per key-server URL) ---

Future<RecoverbullTelemetryServerRow?> fetchServerState(String serverUrl) {
return _sqlite.managers.recoverbullTelemetryServer
.filter((f) => f.serverUrl(serverUrl))
.getSingleOrNull();
}

Future<void> upsertServerState(RecoverbullTelemetryServerRow row) {
return _sqlite
.into(_sqlite.recoverbullTelemetryServer)
.insertOnConflictUpdate(row);
}

// --- Per-backup baselines ---

Future<List<RecoverbullTelemetryBackupRow>> fetchBackups(String serverUrl) {
return _sqlite.managers.recoverbullTelemetryBackup
.filter((f) => f.serverUrl(serverUrl))
.get();
}

Future<RecoverbullTelemetryBackupRow?> fetchBackup(
String serverUrl,
String backupIdHash,
) {
return _sqlite.managers.recoverbullTelemetryBackup
.filter((f) => f.serverUrl(serverUrl) & f.backupIdHash(backupIdHash))
.getSingleOrNull();
}

Future<void> upsertBackup(RecoverbullTelemetryBackupRow row) {
return _sqlite
.into(_sqlite.recoverbullTelemetryBackup)
.insertOnConflictUpdate(row);
}

/// The key-server URL changed: identifiers and snapshots from different
/// servers are unrelated, so every row of the old server is dropped.
Future<void> deleteAllForServer(String serverUrl) async {
await _sqlite.managers.recoverbullTelemetryServer
.filter((f) => f.serverUrl(serverUrl))
.delete();
await _sqlite.managers.recoverbullTelemetryBackup
.filter((f) => f.serverUrl(serverUrl))
.delete();
}

/// App data reset: wipe every telemetry row.
Future<void> clearAll() async {
await _sqlite.managers.recoverbullTelemetryServer.delete();
await _sqlite.managers.recoverbullTelemetryBackup.delete();
}
}
Loading
Loading