⏺ fix: derive nostrGroupId from MLS GroupContext in Welcome processing
The "h" tag is optional in MIP-02 Welcome events — senders like whitenoise-rs omit it. Instead of failing when the h-tag is absent, derive nostrGroupId from the NostrGroupData extension embedded in the Welcome's GroupContext (the authoritative MLS source). An h-tag hint, if present, is still validated against the MLS-derived value.
This commit is contained in:
+6
-6
@@ -369,14 +369,14 @@ private suspend fun processMarmotWelcomeFlow(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val nostrGroupId = innerEvent.nostrGroupId()
|
// "h" tag is optional per MIP-02 — some senders (e.g. whitenoise-rs) omit it.
|
||||||
if (nostrGroupId == null) {
|
// nostrGroupId is derived from the MLS GroupContext's NostrGroupData extension instead.
|
||||||
Log.w("MarmotDbg") { "processMarmotWelcomeFlow: WelcomeEvent missing 'h' tag (nostrGroupId)" }
|
val hintNostrGroupId = innerEvent.nostrGroupId()
|
||||||
return
|
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) {
|
when (result) {
|
||||||
is WelcomeResult.Joined -> {
|
is WelcomeResult.Joined -> {
|
||||||
|
|||||||
+5
-11
@@ -128,20 +128,14 @@ class MarmotManager(
|
|||||||
*/
|
*/
|
||||||
suspend fun processWelcome(
|
suspend fun processWelcome(
|
||||||
welcomeEvent: WelcomeEvent,
|
welcomeEvent: WelcomeEvent,
|
||||||
nostrGroupId: HexKey,
|
hintNostrGroupId: HexKey? = welcomeEvent.nostrGroupId(),
|
||||||
): WelcomeResult {
|
): WelcomeResult {
|
||||||
// Validate that the provided nostrGroupId matches the WelcomeEvent's h-tag if present
|
// nostrGroupId is derived from the MLS GroupContext's NostrGroupData extension.
|
||||||
val eventGroupId = welcomeEvent.nostrGroupId()
|
// The h-tag value (hintNostrGroupId) is validated against the MLS content inside
|
||||||
if (eventGroupId != null && eventGroupId != nostrGroupId) {
|
// inboundProcessor, so senders that omit the h-tag are handled transparently.
|
||||||
return WelcomeResult.Error(
|
val result = inboundProcessor.processWelcome(welcomeEvent, hintNostrGroupId)
|
||||||
"nostrGroupId mismatch: expected $nostrGroupId but WelcomeEvent has $eventGroupId",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val result = inboundProcessor.processWelcome(welcomeEvent, nostrGroupId)
|
|
||||||
|
|
||||||
if (result is WelcomeResult.Joined) {
|
if (result is WelcomeResult.Joined) {
|
||||||
// Update subscription state for the new group
|
|
||||||
subscriptionManager.subscribeGroup(result.nostrGroupId)
|
subscriptionManager.subscribeGroup(result.nostrGroupId)
|
||||||
Log.d("MarmotManager", "Joined group ${result.nostrGroupId}")
|
Log.d("MarmotManager", "Joined group ${result.nostrGroupId}")
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-13
@@ -219,26 +219,21 @@ class MarmotInboundProcessor(
|
|||||||
* 4. Mark KeyPackage as consumed for rotation
|
* 4. Mark KeyPackage as consumed for rotation
|
||||||
*
|
*
|
||||||
* @param welcomeEvent the unwrapped kind:444 event
|
* @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
|
* @return the processing result
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalEncodingApi::class)
|
@OptIn(ExperimentalEncodingApi::class)
|
||||||
suspend fun processWelcome(
|
suspend fun processWelcome(
|
||||||
welcomeEvent: WelcomeEvent,
|
welcomeEvent: WelcomeEvent,
|
||||||
nostrGroupId: HexKey,
|
hintNostrGroupId: HexKey? = null,
|
||||||
): WelcomeResult =
|
): WelcomeResult =
|
||||||
try {
|
try {
|
||||||
com.vitorpamplona.quartz.utils.Log
|
com.vitorpamplona.quartz.utils.Log
|
||||||
.d("MarmotDbg") {
|
.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 welcomeBytes = Base64.decode(welcomeEvent.welcomeBase64())
|
||||||
val keyPackageEventId = welcomeEvent.keyPackageEventId()
|
val keyPackageEventId = welcomeEvent.keyPackageEventId()
|
||||||
@@ -273,10 +268,11 @@ class MarmotInboundProcessor(
|
|||||||
com.vitorpamplona.quartz.utils.Log
|
com.vitorpamplona.quartz.utils.Log
|
||||||
.d("MarmotDbg") { "MarmotInboundProcessor.processWelcome: bundle found — invoking groupManager.processWelcome" }
|
.d("MarmotDbg") { "MarmotInboundProcessor.processWelcome: bundle found — invoking groupManager.processWelcome" }
|
||||||
|
|
||||||
// Join the group
|
// Join the group; nostrGroupId is derived from the MLS GroupContext's
|
||||||
groupManager.processWelcome(nostrGroupId, welcomeBytes, bundle)
|
// NostrGroupData extension. The h-tag hint (if any) is validated inside.
|
||||||
|
val (_, nostrGroupId) = groupManager.processWelcome(welcomeBytes, bundle, hintNostrGroupId)
|
||||||
com.vitorpamplona.quartz.utils.Log
|
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
|
// Mark the KeyPackage as consumed — triggers rotation
|
||||||
keyPackageRotationManager.markConsumedByEventId(keyPackageEventId)
|
keyPackageRotationManager.markConsumedByEventId(keyPackageEventId)
|
||||||
|
|||||||
+22
-7
@@ -192,25 +192,40 @@ class MlsGroupManager(
|
|||||||
* 2. Group state is persisted
|
* 2. Group state is persisted
|
||||||
* 3. A KeyPackage rotation should be triggered (see [needsKeyPackageRotation])
|
* 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 welcomeBytes TLS-serialized Welcome message
|
||||||
* @param bundle the KeyPackageBundle that was used for the invitation
|
* @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(
|
suspend fun processWelcome(
|
||||||
nostrGroupId: HexKey,
|
|
||||||
welcomeBytes: ByteArray,
|
welcomeBytes: ByteArray,
|
||||||
bundle: KeyPackageBundle,
|
bundle: KeyPackageBundle,
|
||||||
): MlsGroup =
|
hintNostrGroupId: HexKey? = null,
|
||||||
|
): Pair<MlsGroup, HexKey> =
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
val group = MlsGroup.processWelcome(welcomeBytes, bundle)
|
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
|
// init_key is consumed — the bundle's initPrivateKey should not be
|
||||||
// reused. Caller must discard the bundle and rotate KeyPackages.
|
// reused. Caller must discard the bundle and rotate KeyPackages.
|
||||||
|
|
||||||
persistGroup(nostrGroupId)
|
persistGroup(derivedId)
|
||||||
group
|
Pair(group, derivedId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user