mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-09 08:04:45 +00:00
fix(desktop): stop falling back to user's connected relays for NIP-17 DMs (P0 security)
Per NIP-17 §Publishing, gift wraps MUST only be published to the relays
advertised in the recipient's kind:10050. Today three send paths in
DesktopIAccount fall through to relayManager.connectedRelays.value
when the recipient has no kind:10050 cached:
sendNip17PrivateMessage (line 200)
sendNip17EncryptedFile (line 231)
sendGiftWraps (line 253)
This is the security-review F-04 metadata leak: at best the wrap never
reaches the recipient (their other clients don't read those relays);
at worst the recipient pubkey + send timestamp leak to general/feed
relays outside their chosen inbox. Same class of bug as the relay-
power-tools work explicitly closed for the relay picker on
2026-04-20 ("block DM fallback to all relays — metadata leak").
Replace the fallback with strict resolution: if the recipient has no
kind:10050 in the cache, return an empty target set. DmSendTracker
already handles total relay count == 0 with a "No relays available"
failure state, so the user gets a visible error instead of a silent
leak.
Indexer fan-out + a UI dialog for the missing-10050 case is the
permanent fix, scoped to Phase 4 (DmInboxRelayResolver). This commit
is the conservative pre-Phase-4 plug — better to fail visibly than
leak silently.
NIP-04 send is unchanged: that path is pre-NIP-17, the encrypted
content sits next to other public events on the sender's outbox by
design.
This commit is contained in:
+32
-36
@@ -35,6 +35,8 @@ import com.vitorpamplona.amethyst.desktop.account.AccountState
|
||||
import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache
|
||||
import com.vitorpamplona.amethyst.desktop.network.RelayConnectionManager
|
||||
import com.vitorpamplona.amethyst.desktop.ui.chats.DmSendTracker
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
||||
@@ -197,18 +199,7 @@ class DesktopIAccount(
|
||||
val batch =
|
||||
result.wraps.map { wrap ->
|
||||
val recipientKey = wrap.recipientPubKey()
|
||||
val targetRelays =
|
||||
if (recipientKey != null) {
|
||||
val dmRelays =
|
||||
localCache
|
||||
.getOrCreateUser(recipientKey)
|
||||
.dmInboxRelays()
|
||||
?.toSet()
|
||||
dmRelays?.ifEmpty { null }
|
||||
?: relayManager.connectedRelays.value
|
||||
} else {
|
||||
relayManager.connectedRelays.value
|
||||
}
|
||||
val targetRelays = resolveDmInboxRelaysStrict(recipientKey)
|
||||
wrap to targetRelays
|
||||
}
|
||||
|
||||
@@ -228,18 +219,7 @@ class DesktopIAccount(
|
||||
val batch =
|
||||
result.wraps.map { wrap ->
|
||||
val recipientKey = wrap.recipientPubKey()
|
||||
val targetRelays =
|
||||
if (recipientKey != null) {
|
||||
val dmRelays =
|
||||
localCache
|
||||
.getOrCreateUser(recipientKey)
|
||||
.dmInboxRelays()
|
||||
?.toSet()
|
||||
dmRelays?.ifEmpty { null }
|
||||
?: relayManager.connectedRelays.value
|
||||
} else {
|
||||
relayManager.connectedRelays.value
|
||||
}
|
||||
val targetRelays = resolveDmInboxRelaysStrict(recipientKey)
|
||||
wrap to targetRelays
|
||||
}
|
||||
|
||||
@@ -250,24 +230,40 @@ class DesktopIAccount(
|
||||
val batch =
|
||||
wraps.map { wrap ->
|
||||
val recipientKey = wrap.recipientPubKey()
|
||||
val targetRelays =
|
||||
if (recipientKey != null) {
|
||||
val dmRelays =
|
||||
localCache
|
||||
.getOrCreateUser(recipientKey)
|
||||
.dmInboxRelays()
|
||||
?.toSet()
|
||||
dmRelays?.ifEmpty { null }
|
||||
?: relayManager.connectedRelays.value
|
||||
} else {
|
||||
relayManager.connectedRelays.value
|
||||
}
|
||||
val targetRelays = resolveDmInboxRelaysStrict(recipientKey)
|
||||
wrap to targetRelays
|
||||
}
|
||||
|
||||
scope.launch { dmSendTracker.sendBatch(batch) }
|
||||
}
|
||||
|
||||
/**
|
||||
* NIP-17 inbox-relay resolution, strict variant — no fallback to the
|
||||
* user's connected relays.
|
||||
*
|
||||
* Per NIP-17 §Publishing, a gift wrap MUST only land on relays advertised
|
||||
* in the recipient's kind:10050. Falling back to the sender's connected
|
||||
* relays when 10050 is missing publishes the wrap to relays the recipient
|
||||
* does NOT consult — at best the message never arrives, at worst it leaks
|
||||
* the conversation metadata (recipient pubkey + send timestamp) to relays
|
||||
* outside the recipient's chosen inbox.
|
||||
*
|
||||
* Empty result means the wrap will not be sent; [DmSendTracker.sendBatch]
|
||||
* surfaces this as a "No relays available" failure to the user. Indexer
|
||||
* fan-out + a UI prompt for the missing-10050 case lands with the
|
||||
* [DmInboxRelayResolver] (Phase 4); until then "no 10050 → cannot send"
|
||||
* is the conservative position.
|
||||
*/
|
||||
private fun resolveDmInboxRelaysStrict(recipientKey: HexKey?): Set<NormalizedRelayUrl> {
|
||||
if (recipientKey == null) return emptySet()
|
||||
return localCache
|
||||
.getOrCreateUser(recipientKey)
|
||||
.dmInboxRelays()
|
||||
?.toSet()
|
||||
?.ifEmpty { null }
|
||||
?: emptySet()
|
||||
}
|
||||
|
||||
private fun addEventToChatroom(
|
||||
event: com.vitorpamplona.quartz.nip01Core.core.Event,
|
||||
roomKey: com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey,
|
||||
|
||||
Reference in New Issue
Block a user