mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
fix(mobile): use only the verified envelope for a repeated inventory ID
The verification cache was keyed by event ID but then read the current envelope, so a relay could resend a verified ID with a different d tag and skip verification. A repeated ID now uses the envelope verified for it, and a regression covers both orders of the forged copy. Signed-off-by: Tom Brow <tomb@block.xyz>
This commit is contained in:
@@ -105,7 +105,7 @@ Future<List<String>> _managedAgentPubkeys(
|
||||
String? agent,
|
||||
}) async {
|
||||
final heads = <String, NostrEvent>{};
|
||||
final verified = <String>{};
|
||||
final verified = <String, NostrEvent>{};
|
||||
NostrEvent? cursor;
|
||||
for (var page = 0; ; page++) {
|
||||
if (page == _maxInventoryPages) {
|
||||
@@ -135,21 +135,23 @@ Future<List<String>> _managedAgentPubkeys(
|
||||
throw StateError('Agent inventory response is outside its owner.');
|
||||
}
|
||||
for (final event in events) {
|
||||
final d = event.getTagValue('d');
|
||||
if (d == null || (agent != null && d != agent)) continue;
|
||||
// Like desktop, skip an envelope that fails verification so a forged
|
||||
// record can neither add an agent nor supersede a genuine one. An ID
|
||||
// already verified needs no second check when pages overlap.
|
||||
if (!verified.contains(event.id)) {
|
||||
// record can neither add an agent nor supersede a genuine one. When
|
||||
// pages overlap, a repeated ID uses only the envelope already verified
|
||||
// for it, never the bytes the relay sent again.
|
||||
var record = verified[event.id];
|
||||
if (record == null) {
|
||||
try {
|
||||
nostr.Event.fromJson(jsonEncode(event.toJson()));
|
||||
} catch (_) {
|
||||
continue;
|
||||
}
|
||||
verified.add(event.id);
|
||||
record = verified[event.id] = event;
|
||||
}
|
||||
final d = record.getTagValue('d');
|
||||
if (d == null || (agent != null && d != agent)) continue;
|
||||
final previous = heads[d];
|
||||
if (previous == null || _newer(event, previous)) heads[d] = event;
|
||||
if (previous == null || _newer(record, previous)) heads[d] = record;
|
||||
}
|
||||
if (events.length < _pageSize) break;
|
||||
// Relay order is newest first, then ID ascending at ties.
|
||||
|
||||
@@ -452,6 +452,39 @@ void main() {
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
for (final forgedFirst in [false, true]) {
|
||||
test(
|
||||
'a repeated ID cannot inject an agent (forged first: $forgedFirst)',
|
||||
() async {
|
||||
final genuine = managed(agent.public);
|
||||
final forged = NostrEvent.fromJson(
|
||||
genuine.toJson()
|
||||
..['tags'] = [
|
||||
['d', other.public],
|
||||
],
|
||||
);
|
||||
final relay = _Relay(
|
||||
inventory: [
|
||||
forgedFirst ? [forged, genuine] : [genuine, forged],
|
||||
],
|
||||
profiles: [
|
||||
profile(agent, ownedBy: owner),
|
||||
profile(other, ownedBy: owner),
|
||||
],
|
||||
);
|
||||
final container = setup(relay);
|
||||
expect(
|
||||
(await container.read(
|
||||
meAssistantsProvider.future,
|
||||
)).map((a) => a.pubkey),
|
||||
[agent.public],
|
||||
);
|
||||
final sync = container.read(meSyncProvider);
|
||||
expect(await isOwnedMeAssistant(sync, other.public), isFalse);
|
||||
expect(await isOwnedMeAssistant(sync, agent.public), isTrue);
|
||||
},
|
||||
);
|
||||
}
|
||||
test(
|
||||
'creation rejects revoked ownership and unrelated returned profiles',
|
||||
() async {
|
||||
|
||||
Reference in New Issue
Block a user