feat(concord): send & receive encrypted image messages (Armada-compatible)

Concord channel messages can now carry images, wire-identical to Soapbox
Armada's `encryptAttachments`: a normal channel-bound kind-9 whose ciphertext
URL is appended to the content and annotated by a NIP-92 `imeta` tag with
`encryption-algorithm aes-gcm`, hex `decryption-key`/`decryption-nonce`, and the
plaintext `ox` hash (no `x`). The blob is AES-256-GCM ciphertext on Blossom, so
the media host and relays only ever see encrypted bytes — the community's E2E
guarantee holds.

Reuses the NIP-17 encrypted-media stack end to end: quartz's imeta tag vocab
and IMetaTagBuilder to build/parse the tag (ChannelChat.imageMessage /
encryptedImageImeta / encryptedImagesOf), the shared UploadOrchestrator
encrypted upload + ChatFileUploadDialog picker on the send side, and the OkHttp
EncryptedBlobInterceptor keyCache on the receive side — registering each
attachment's cipher (keyed by URL) lets the normal feed renderer display the
decrypted image with no shared-render changes. Encryption is mandatory
(no toggle, and a missing cipher fails closed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
This commit is contained in:
Claude
2026-07-14 19:44:56 +00:00
parent e231b0cf5b
commit e734d6400c
7 changed files with 351 additions and 0 deletions
@@ -48,6 +48,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip92IMeta.IMetaTag
import com.vitorpamplona.quartz.nipC7Chats.ChatEvent
/** One decrypted, verified Concord channel message projected for display. */
@@ -160,6 +161,23 @@ object ConcordActions {
return ConcordStreamEnvelope.wrap(rumor, channel, authorSigner, encrypted = true)
}
/**
* Builds an encrypted-seal channel message wrap carrying one or more encrypted image [imetas]
* (Armada `encryptAttachments` shape) to publish on the [channel] plane.
*/
suspend fun buildChannelImageMessage(
authorSigner: NostrSigner,
channel: GroupKey,
channelId: HexKey,
epoch: Long,
text: String,
imetas: List<IMetaTag>,
createdAt: Long,
): Event {
val rumor = ChannelChat.imageMessage(authorSigner.pubKey, channelId, epoch, text, imetas, createdAt)
return ConcordStreamEnvelope.wrap(rumor, channel, authorSigner, encrypted = true)
}
/** Builds an encrypted-seal inline quote-reply wrap (kind-9 message quoting [parent] via `q`) on the [channel] plane. */
suspend fun buildChannelInlineReply(
authorSigner: NostrSigner,