mirror of
https://github.com/block/buzz.git
synced 2026-11-03 16:01:35 +00:00
Use shared fail-closed Android secure-storage options across all namespace clients. Allow token-only legacy records through cloud recovery, clarify consent, and prevent stale chooser taps from interrupting cancellation. Exercise real Android plugin decryption failures with an isolated emulator probe, and cover actual link-entry consent and lifecycle guards. Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz> Signed-off-by: Wes <wesbillman@users.noreply.github.com>
85 lines
2.9 KiB
Dart
85 lines
2.9 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|
|
|
import '../storage/secure_storage.dart';
|
|
|
|
import 'account_identity_store.dart';
|
|
|
|
/// Public identity choices; never contains private signing material.
|
|
class DeviceIdentityChoice {
|
|
const DeviceIdentityChoice(this.pubkey, this.label, {this.fromCloud = false});
|
|
final String pubkey;
|
|
final String label;
|
|
final bool fromCloud;
|
|
}
|
|
|
|
/// Read-only inventory, including legacy records hidden by a newer community list.
|
|
/// Do not call CommunityStorage.loadAll here: it migrates and deletes records.
|
|
class DeviceIdentitySource {
|
|
DeviceIdentitySource({FlutterSecureStorage? storage})
|
|
: _storage = storage ?? communitySecureStorage;
|
|
final FlutterSecureStorage _storage;
|
|
|
|
Future<List<DeviceIdentityChoice>> choices() async => [
|
|
for (final entry in (await _read()).entries)
|
|
DeviceIdentityChoice(entry.key, entry.value.label),
|
|
];
|
|
|
|
/// Reread and validate the chosen key immediately before create-only adoption.
|
|
Future<AccountIdentitySnapshot> adopt(
|
|
String pubkey,
|
|
AccountIdentityStore store, {
|
|
required void Function() checkCurrent,
|
|
}) async {
|
|
final candidate = (await _read())[pubkey];
|
|
if (candidate == null) throw AccountIdentityFailure.absent;
|
|
checkCurrent();
|
|
return store.create(candidate.nsec, checkCurrent: checkCurrent);
|
|
}
|
|
|
|
Future<Map<String, ({String nsec, String label})>> _read() async {
|
|
try {
|
|
final result = <String, ({String nsec, String label})>{};
|
|
final labels = <String, Set<String>>{};
|
|
void add(dynamic nsec, dynamic advertised, String label) {
|
|
if (nsec == null) {
|
|
// Token-only legacy communities legitimately have no private key.
|
|
return;
|
|
}
|
|
if (nsec is! String) throw AccountIdentityFailure.unreadable;
|
|
final pubkey = AccountIdentityStore.publicKeyForNsec(nsec);
|
|
if (advertised != null && advertised != pubkey) {
|
|
throw AccountIdentityFailure.unreadable;
|
|
}
|
|
final names = labels.putIfAbsent(pubkey, () => <String>{})..add(label);
|
|
result[pubkey] = (nsec: nsec, label: names.join(', '));
|
|
}
|
|
|
|
for (final key in ['buzz_communities', 'buzz_workspaces']) {
|
|
final raw = await _storage.read(key: key);
|
|
if (raw == null) continue;
|
|
final records = jsonDecode(raw) as List;
|
|
for (final record in records) {
|
|
final value = record as Map<String, dynamic>;
|
|
add(
|
|
value['nsec'],
|
|
value['pubkey'],
|
|
(value['name'] as String?)?.trim().isNotEmpty == true
|
|
? (value['name'] as String).trim()
|
|
: 'Saved community',
|
|
);
|
|
}
|
|
}
|
|
add(
|
|
await _storage.read(key: 'buzz_nsec'),
|
|
await _storage.read(key: 'buzz_pubkey'),
|
|
'Previous device identity',
|
|
);
|
|
return result;
|
|
} catch (_) {
|
|
throw AccountIdentityFailure.unreadable;
|
|
}
|
|
}
|
|
}
|