refactor(commons): share NIP-59 gift-wrap+seal unwrap helper
Three call sites did the same two-layer peel — kind:1059 → (nip44 decrypt) → kind:13 sealed rumor → (nip44 decrypt) → rumor Extracted as `GiftWrapEvent.unwrapAndUnsealOrNull(signer)` in commons/relayClient/nip17Dm/, and collapsed: - commons/marmot/MarmotIngest.kt — was an inline `when` switch; now a one-liner. Behaviour unchanged (still passes through if the inner isn't a seal, matching the previous defensive `else -> inner` branch). - desktopApp/Main.kt — dropped the nested unwrap/unseal block in the kind:1059 case of the LocalCache dispatcher. - cli/DmCommands.kt — replaced the manual chain in `decryptChatMessages`. Android's DecryptAndIndexProcessor keeps its two separate handlers (processNewGiftWrap / processNewSealedRumor) because the LocalCache pipeline re-enters on each peel — that's a different shape and not touched. No behaviour change on any platform.
This commit is contained in:
@@ -26,13 +26,13 @@ import com.vitorpamplona.amethyst.cli.Context
|
|||||||
import com.vitorpamplona.amethyst.cli.DataDir
|
import com.vitorpamplona.amethyst.cli.DataDir
|
||||||
import com.vitorpamplona.amethyst.cli.Json
|
import com.vitorpamplona.amethyst.cli.Json
|
||||||
import com.vitorpamplona.amethyst.commons.relayClient.nip17Dm.filterGiftWrapsToPubkey
|
import com.vitorpamplona.amethyst.commons.relayClient.nip17Dm.filterGiftWrapsToPubkey
|
||||||
|
import com.vitorpamplona.amethyst.commons.relayClient.nip17Dm.unwrapAndUnsealOrNull
|
||||||
import com.vitorpamplona.quartz.marmot.RecipientRelayFetcher
|
import com.vitorpamplona.quartz.marmot.RecipientRelayFetcher
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||||
import com.vitorpamplona.quartz.nip17Dm.NIP17Factory
|
import com.vitorpamplona.quartz.nip17Dm.NIP17Factory
|
||||||
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
|
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
|
||||||
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
|
|
||||||
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
@@ -258,8 +258,7 @@ object DmCommands {
|
|||||||
val out = mutableListOf<DecryptedDm>()
|
val out = mutableListOf<DecryptedDm>()
|
||||||
for ((relay, event) in raw) {
|
for ((relay, event) in raw) {
|
||||||
if (event !is GiftWrapEvent) continue
|
if (event !is GiftWrapEvent) continue
|
||||||
val sealed = event.unwrapOrNull(ctx.signer) as? SealedRumorEvent ?: continue
|
val inner = event.unwrapAndUnsealOrNull(ctx.signer) as? ChatMessageEvent ?: continue
|
||||||
val inner = sealed.unsealOrNull(ctx.signer) as? ChatMessageEvent ?: continue
|
|
||||||
if (!seen.add(inner.id)) continue
|
if (!seen.add(inner.id)) continue
|
||||||
val members = inner.groupMembers()
|
val members = inner.groupMembers()
|
||||||
if (peerHex != null && peerHex !in members) continue
|
if (peerHex != null && peerHex !in members) continue
|
||||||
|
|||||||
+7
-14
@@ -20,6 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.commons.marmot
|
package com.vitorpamplona.amethyst.commons.marmot
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.commons.relayClient.nip17Dm.unwrapAndUnsealOrNull
|
||||||
import com.vitorpamplona.quartz.marmot.GroupEventResult
|
import com.vitorpamplona.quartz.marmot.GroupEventResult
|
||||||
import com.vitorpamplona.quartz.marmot.MarmotInboundProcessor
|
import com.vitorpamplona.quartz.marmot.MarmotInboundProcessor
|
||||||
import com.vitorpamplona.quartz.marmot.WelcomeResult
|
import com.vitorpamplona.quartz.marmot.WelcomeResult
|
||||||
@@ -27,7 +28,6 @@ import com.vitorpamplona.quartz.marmot.mip02Welcome.WelcomeEvent
|
|||||||
import com.vitorpamplona.quartz.marmot.mip03GroupMessages.GroupEvent
|
import com.vitorpamplona.quartz.marmot.mip03GroupMessages.GroupEvent
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
|
|
||||||
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -96,19 +96,12 @@ suspend fun MarmotManager.ingest(event: Event): MarmotIngestResult =
|
|||||||
|
|
||||||
private suspend fun MarmotManager.ingestGiftWrap(wrap: GiftWrapEvent): MarmotIngestResult =
|
private suspend fun MarmotManager.ingestGiftWrap(wrap: GiftWrapEvent): MarmotIngestResult =
|
||||||
try {
|
try {
|
||||||
// NIP-59 wraps contain TWO encryption layers:
|
// NIP-59 wraps carry two encryption layers (kind:1059 → kind:13 → rumor).
|
||||||
// kind:1059 gift wrap → kind:13 sealed rumor → the rumor itself.
|
// [unwrapAndUnsealOrNull] peels both so we land directly on the inner
|
||||||
// `GiftWrapEvent.unwrapOrNull` only peels the outer layer; when the
|
// kind:444 Welcome rumor. Checking `isWelcomeEvent` on the seal itself
|
||||||
// result is a [SealedRumorEvent] we must unseal it to reach the
|
// (the old bug) always took the Ignored branch and silently dropped
|
||||||
// kind:444 Welcome rumor. The old code checked `isWelcomeEvent` on
|
// every inbound Welcome.
|
||||||
// the seal (kind:13) and always took the Ignored branch, which is
|
val rumor = wrap.unwrapAndUnsealOrNull(signer) ?: return MarmotIngestResult.Ignored
|
||||||
// why every inbound Welcome was silently dropped by the CLI and by
|
|
||||||
// any non-Amethyst consumer.
|
|
||||||
val rumor =
|
|
||||||
when (val inner = wrap.unwrapOrNull(signer) ?: return MarmotIngestResult.Ignored) {
|
|
||||||
is SealedRumorEvent -> inner.unsealOrNull(signer) ?: return MarmotIngestResult.Ignored
|
|
||||||
else -> inner
|
|
||||||
}
|
|
||||||
if (!MarmotInboundProcessor.isWelcomeEvent(rumor) || rumor !is WelcomeEvent) {
|
if (!MarmotInboundProcessor.isWelcomeEvent(rumor) || rumor !is WelcomeEvent) {
|
||||||
return MarmotIngestResult.Ignored
|
return MarmotIngestResult.Ignored
|
||||||
}
|
}
|
||||||
|
|||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.commons.relayClient.nip17Dm
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||||
|
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
|
||||||
|
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fully decrypt a NIP-59 gift wrap down to the inner rumor.
|
||||||
|
*
|
||||||
|
* A gift wrap carries two encryption layers:
|
||||||
|
* kind:1059 [GiftWrapEvent] → kind:13 [SealedRumorEvent] → the rumor.
|
||||||
|
*
|
||||||
|
* [GiftWrapEvent.unwrapOrNull] peels only the outer layer. Every non-Android
|
||||||
|
* consumer that looks at the rumor directly (the CLI's `dm list`, the desktop
|
||||||
|
* chat receive path, the Marmot welcome ingest in commons) needs both peels.
|
||||||
|
*
|
||||||
|
* If the inner content isn't a [SealedRumorEvent] (malformed, or a future
|
||||||
|
* NIP-59 variant that wraps a rumor directly), the outer unwrap result is
|
||||||
|
* returned as-is so callers can still route on kind. Either unwrap returning
|
||||||
|
* null propagates as null.
|
||||||
|
*/
|
||||||
|
suspend fun GiftWrapEvent.unwrapAndUnsealOrNull(signer: NostrSigner): Event? {
|
||||||
|
val inner = unwrapOrNull(signer) ?: return null
|
||||||
|
return if (inner is SealedRumorEvent) inner.unsealOrNull(signer) else inner
|
||||||
|
}
|
||||||
@@ -71,6 +71,7 @@ import androidx.compose.ui.window.Window
|
|||||||
import androidx.compose.ui.window.WindowPosition
|
import androidx.compose.ui.window.WindowPosition
|
||||||
import androidx.compose.ui.window.application
|
import androidx.compose.ui.window.application
|
||||||
import androidx.compose.ui.window.rememberWindowState
|
import androidx.compose.ui.window.rememberWindowState
|
||||||
|
import com.vitorpamplona.amethyst.commons.relayClient.nip17Dm.unwrapAndUnsealOrNull
|
||||||
import com.vitorpamplona.amethyst.desktop.account.AccountManager
|
import com.vitorpamplona.amethyst.desktop.account.AccountManager
|
||||||
import com.vitorpamplona.amethyst.desktop.account.AccountState
|
import com.vitorpamplona.amethyst.desktop.account.AccountState
|
||||||
import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache
|
import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache
|
||||||
@@ -1022,13 +1023,9 @@ fun MainContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
is com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent -> {
|
is com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent -> {
|
||||||
// NIP-17: unwrap gift wrap → seal → inner event
|
// NIP-17: peel both the gift-wrap and sealed-rumor layers.
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val seal =
|
val innerEvent = event.unwrapAndUnsealOrNull(iAccount.signer) ?: return@launch
|
||||||
event.unwrapOrNull(iAccount.signer)
|
|
||||||
as? com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
|
|
||||||
?: return@launch
|
|
||||||
val innerEvent = seal.unsealOrNull(iAccount.signer) ?: return@launch
|
|
||||||
when (innerEvent) {
|
when (innerEvent) {
|
||||||
is com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent -> {
|
is com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent -> {
|
||||||
val innerNote = localCache.getOrCreateNote(innerEvent.id)
|
val innerNote = localCache.getOrCreateNote(innerEvent.id)
|
||||||
|
|||||||
Reference in New Issue
Block a user