diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt index 62356c6ee0..9e24c48ed6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt @@ -369,14 +369,14 @@ private suspend fun processMarmotWelcomeFlow( return } - val nostrGroupId = innerEvent.nostrGroupId() - if (nostrGroupId == null) { - Log.w("MarmotDbg") { "processMarmotWelcomeFlow: WelcomeEvent missing 'h' tag (nostrGroupId)" } - return + // "h" tag is optional per MIP-02 — some senders (e.g. whitenoise-rs) omit it. + // nostrGroupId is derived from the MLS GroupContext's NostrGroupData extension instead. + val hintNostrGroupId = innerEvent.nostrGroupId() + Log.d("MarmotDbg") { + "processMarmotWelcomeFlow: h-tag=${hintNostrGroupId?.take(8) ?: "(absent)"} — deriving from MLS content" } - Log.d("MarmotDbg") { "processMarmotWelcomeFlow: invoking manager.processWelcome group=${nostrGroupId.take(8)}…" } - val result = manager.processWelcome(innerEvent, nostrGroupId) + val result = manager.processWelcome(innerEvent, hintNostrGroupId) when (result) { is WelcomeResult.Joined -> { diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/marmot/MarmotManager.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/marmot/MarmotManager.kt index a42d78e721..8aaac3ae69 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/marmot/MarmotManager.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/marmot/MarmotManager.kt @@ -128,20 +128,14 @@ class MarmotManager( */ suspend fun processWelcome( welcomeEvent: WelcomeEvent, - nostrGroupId: HexKey, + hintNostrGroupId: HexKey? = welcomeEvent.nostrGroupId(), ): WelcomeResult { - // Validate that the provided nostrGroupId matches the WelcomeEvent's h-tag if present - val eventGroupId = welcomeEvent.nostrGroupId() - if (eventGroupId != null && eventGroupId != nostrGroupId) { - return WelcomeResult.Error( - "nostrGroupId mismatch: expected $nostrGroupId but WelcomeEvent has $eventGroupId", - ) - } - - val result = inboundProcessor.processWelcome(welcomeEvent, nostrGroupId) + // nostrGroupId is derived from the MLS GroupContext's NostrGroupData extension. + // The h-tag value (hintNostrGroupId) is validated against the MLS content inside + // inboundProcessor, so senders that omit the h-tag are handled transparently. + val result = inboundProcessor.processWelcome(welcomeEvent, hintNostrGroupId) if (result is WelcomeResult.Joined) { - // Update subscription state for the new group subscriptionManager.subscribeGroup(result.nostrGroupId) Log.d("MarmotManager", "Joined group ${result.nostrGroupId}") } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/MarmotInboundProcessor.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/MarmotInboundProcessor.kt index 893ab7215a..92725aa918 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/MarmotInboundProcessor.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/MarmotInboundProcessor.kt @@ -219,26 +219,21 @@ class MarmotInboundProcessor( * 4. Mark KeyPackage as consumed for rotation * * @param welcomeEvent the unwrapped kind:444 event - * @param nostrGroupId the Nostr group ID (from relay context or Welcome tags) + * @param hintNostrGroupId optional group ID from the "h" tag; validated against MLS content + * if provided. If absent (sender omitted "h" tag), the ID is derived from the Welcome's + * NostrGroupData extension — the MLS content is always the authoritative source. * @return the processing result */ @OptIn(ExperimentalEncodingApi::class) suspend fun processWelcome( welcomeEvent: WelcomeEvent, - nostrGroupId: HexKey, + hintNostrGroupId: HexKey? = null, ): WelcomeResult = try { com.vitorpamplona.quartz.utils.Log .d("MarmotDbg") { - "MarmotInboundProcessor.processWelcome: group=${nostrGroupId.take(8)}… eventId=${welcomeEvent.id.take(8)}…" + "MarmotInboundProcessor.processWelcome: hint=${hintNostrGroupId?.take(8)} eventId=${welcomeEvent.id.take(8)}…" } - // Validate the caller-provided nostrGroupId matches the Welcome event's own h tag - val eventGroupId = welcomeEvent.nostrGroupId() - if (eventGroupId != null && eventGroupId != nostrGroupId) { - return WelcomeResult.Error( - "nostrGroupId mismatch: caller=$nostrGroupId, event=$eventGroupId", - ) - } val welcomeBytes = Base64.decode(welcomeEvent.welcomeBase64()) val keyPackageEventId = welcomeEvent.keyPackageEventId() @@ -273,10 +268,11 @@ class MarmotInboundProcessor( com.vitorpamplona.quartz.utils.Log .d("MarmotDbg") { "MarmotInboundProcessor.processWelcome: bundle found — invoking groupManager.processWelcome" } - // Join the group - groupManager.processWelcome(nostrGroupId, welcomeBytes, bundle) + // Join the group; nostrGroupId is derived from the MLS GroupContext's + // NostrGroupData extension. The h-tag hint (if any) is validated inside. + val (_, nostrGroupId) = groupManager.processWelcome(welcomeBytes, bundle, hintNostrGroupId) com.vitorpamplona.quartz.utils.Log - .d("MarmotDbg") { "MarmotInboundProcessor.processWelcome: groupManager.processWelcome succeeded for ${nostrGroupId.take(8)}…" } + .d("MarmotDbg") { "MarmotInboundProcessor.processWelcome: joined group=${nostrGroupId.take(8)}…" } // Mark the KeyPackage as consumed — triggers rotation keyPackageRotationManager.markConsumedByEventId(keyPackageEventId) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mls/group/MlsGroupManager.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mls/group/MlsGroupManager.kt index 3b689b1149..422fd0ad24 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mls/group/MlsGroupManager.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mls/group/MlsGroupManager.kt @@ -192,25 +192,40 @@ class MlsGroupManager( * 2. Group state is persisted * 3. A KeyPackage rotation should be triggered (see [needsKeyPackageRotation]) * - * @param nostrGroupId hex-encoded Nostr group ID (from Welcome event tags) * @param welcomeBytes TLS-serialized Welcome message * @param bundle the KeyPackageBundle that was used for the invitation - * @return the joined [MlsGroup] + * @param hintNostrGroupId optional nostrGroupId from the Welcome event's "h" tag; + * if provided and non-null, validated against the GroupContext's NostrGroupData extension. + * If absent (sender did not include an "h" tag), the ID is derived from the MLS content. + * @return pair of (joined group, derived nostrGroupId) */ suspend fun processWelcome( - nostrGroupId: HexKey, welcomeBytes: ByteArray, bundle: KeyPackageBundle, - ): MlsGroup = + hintNostrGroupId: HexKey? = null, + ): Pair = mutex.withLock { val group = MlsGroup.processWelcome(welcomeBytes, bundle) - groups[nostrGroupId] = group + + val derivedId = + group.currentMarmotData()?.nostrGroupId + ?: throw IllegalArgumentException( + "Welcome GroupContext is missing the NostrGroupData extension — cannot derive nostrGroupId", + ) + + if (hintNostrGroupId != null && hintNostrGroupId != derivedId) { + throw IllegalArgumentException( + "nostrGroupId mismatch: h-tag=$hintNostrGroupId, GroupContext=$derivedId", + ) + } + + groups[derivedId] = group // init_key is consumed — the bundle's initPrivateKey should not be // reused. Caller must discard the bundle and rotate KeyPackages. - persistGroup(nostrGroupId) - group + persistGroup(derivedId) + Pair(group, derivedId) } /**