diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/concord/envelope/ConcordStreamEnvelope.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/concord/envelope/ConcordStreamEnvelope.kt index a2bda26d86..a3e9ee9172 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/concord/envelope/ConcordStreamEnvelope.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/concord/envelope/ConcordStreamEnvelope.kt @@ -32,6 +32,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip44Encryption.Nip44 import com.vitorpamplona.quartz.nip44Encryption.Nip44v2 +import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor import com.vitorpamplona.quartz.utils.TimeUtils /** @@ -78,11 +79,14 @@ object ConcordStreamEnvelope { authorSigner: NostrSigner, encrypted: Boolean, ): Event { + // A rumor is unsigned (NIP-59): serialize it without `sig`. applesauce clients read + // `"sig": ""` as a signed event with a bad signature and drop it. + val rumorJson = Rumor.toJson(Rumor.create(rumor)) val content = if (encrypted) { - encryptChecked(rumor.toJson(), stream.conversationKey) + encryptChecked(rumorJson, stream.conversationKey) } else { - rumor.toJson() + rumorJson } val kind = if (encrypted) KIND_SEAL_ENCRYPTED else KIND_SEAL_PLAINTEXT return authorSigner.sign(rumor.createdAt, kind, EMPTY_TAGS, content) diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/concord/envelope/ConcordStreamEnvelopeTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/concord/envelope/ConcordStreamEnvelopeTest.kt index e9fc72f3fb..50e0f9e1fa 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/concord/envelope/ConcordStreamEnvelopeTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/concord/envelope/ConcordStreamEnvelopeTest.kt @@ -28,8 +28,12 @@ import com.vitorpamplona.quartz.nip01Core.crypto.verify import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorAssembler import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertNull import kotlin.test.assertTrue @@ -69,6 +73,21 @@ class ConcordStreamEnvelopeTest { assertEquals(9, opened.rumor.kind) } + /** + * A rumor is unsigned (NIP-59): its JSON carries no `sig`. applesauce-based clients + * (Accordion) treat an object with `"sig": ""` as a signed event whose signature fails, + * and silently dropped every Concord message and Guestbook Join we sent. + */ + @Test + fun sealedRumorJsonCarriesNoSig() = + runTest { + val seal = ConcordStreamEnvelope.seal(chatRumor("no sig"), stream, authorSigner, encrypted = false) + val rumorJson = Json.parseToJsonElement(seal.content).jsonObject + + assertFalse("sig" in rumorJson, "rumor JSON must not carry a sig: ${seal.content}") + assertEquals(chatRumor("no sig").id, rumorJson["id"]?.jsonPrimitive?.content) + } + @Test fun encryptedSealRoundTrips() = runTest {