fix: stop discarding gift wraps addressed to someone else
An inbound kind:1059 whose `p` tag is not our pubkey took down the entire
save transaction:
java.lang.IllegalStateException: Invalid Mac: Calculated f1db537e…, decoded: 45c8c86a…
at com.vitorpamplona.quartz.nip44Encryption.crypto.Hkdf.fastExpand
at com.vitorpamplona.quartz.nip44Encryption.Nip44v2.checkMessageKeys
…
at press.mantra.compose.database.model.GiftWrapMessage.decryptGiftWrapSeal
at press.mantra.compose.database.dao.NostrDao.indexNostrEvent
at press.mantra.compose.database.dao.NostrDao.storeNostrEvent
Two separate things were wrong.
The first is that decryptGiftWrapSeal attempted the decryption at all. When
the recipient did not match our key it logged "We are unwrapping a message we
may have sent" and called
Nip44.decrypt(content, privateKey = ourPrivKey, pubKey = giftWrapEvent.pubKey)
giftWrapEvent.pubKey is the wrap's ephemeral author. NIP-59 encrypts the wrap
under ECDH(ephemeralPriv, recipientPub), and GiftWrapEvent.create mints that
ephemeral key with NostrSignerSync(KeyPair()) and discards it on return.
ECDH(ourPriv, ephemeralPub) is a third, unrelated key, so the MAC check could
never pass. A sender genuinely cannot unwrap their own gift wrap; that is the
point of the construction, not a gap in it. The call threw its result away
anyway (keyPair.privKey?.let { …; null }) and fell through to the trailing
`return null`, so it was a probe whose only possible outcome was an exception.
The second is that a null seal was treated as a failure. indexNostrEvent
throws GiftWrapUnsealException on null, which unwinds out of the Room
transaction in storeNostrEvent and rolls back everything written for the
event: the NostrEvent row, its NostrEventRelay row, and the GiftWrapMessage
upserted moments earlier. The only catch sits in DatabaseNostrRepository,
which logs and continues — and that catch also swallows the
`status = "processed"` upsert on the SynchronizeNostrEventRequest, so the
event was re-fetched and re-failed on every later sync pass.
isAddressedTo now answers the question with no crypto at all, and the indexer
returns early for wraps that are not ours: the event and the wrap row survive,
the remainder of indexNostrEvent still runs, the transaction commits, and the
sync request is marked processed. GiftWrapUnsealException goes back to meaning
what it says — addressed to us, but unsealing failed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -523,6 +523,15 @@ abstract class NostrDao(
|
||||
giftWrapMessage
|
||||
)
|
||||
|
||||
if (!giftWrapMessage.isAddressedTo(activeKeyPair)) {
|
||||
// Undecryptable by design rather than by failure, so keep the event and
|
||||
// the wrap we just stored and stop here. Throwing would roll the whole
|
||||
// transaction back and lose both.
|
||||
logger.d("GiftWrap ${nostrEvent.id} is addressed to ${giftWrapMessage.receiverPublicKey}, nothing to index")
|
||||
|
||||
return@let
|
||||
}
|
||||
|
||||
giftWrapMessage.decryptGiftWrapSeal(
|
||||
activeKeyPair
|
||||
).let { giftWrapSeal ->
|
||||
|
||||
@@ -7,13 +7,11 @@ import androidx.room3.PrimaryKey
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Kind
|
||||
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||
import com.vitorpamplona.quartz.nip44Encryption.Nip44
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
@@ -83,6 +81,18 @@ data class GiftWrapMessage(
|
||||
@Ignore
|
||||
private val logger = Logger.withTag("GiftWrapMessage")
|
||||
|
||||
/**
|
||||
* Whether this gift wrap is addressed to [keyPair], i.e. whether we hold the
|
||||
* private key that can unwrap it.
|
||||
*
|
||||
* NIP-59 encrypts the wrap to its recipient using an ephemeral key that
|
||||
* [GiftWrapEvent.create] throws away, so a wrap addressed to anyone else can
|
||||
* never be decrypted by us, not even one we sent ourselves.
|
||||
*/
|
||||
fun isAddressedTo(
|
||||
keyPair: KeyPair
|
||||
): Boolean = receiverPublicKey.equals(keyPair.pubKey.toHexKey(), ignoreCase = true)
|
||||
|
||||
suspend fun decryptGiftWrapSeal(
|
||||
keyPair: KeyPair
|
||||
): GiftWrapSeal? {
|
||||
@@ -105,19 +115,12 @@ data class GiftWrapMessage(
|
||||
giftWrapEvent.recipientPubKey()?.let { recipientPublicKey ->
|
||||
logger.d("Recipient PublicKey: $recipientPublicKey")
|
||||
|
||||
if (keyPair.pubKey.toHexKey() != recipientPublicKey) {
|
||||
logger.e("We are unwrapping a message we may have sent from ${keyPair.pubKey.toHexKey()}")
|
||||
|
||||
keyPair.privKey?.let { privateKey ->
|
||||
val sealJSON = Nip44.decrypt(
|
||||
giftWrapEvent.content,
|
||||
privateKey = privateKey,
|
||||
pubKey = giftWrapEvent.pubKey.hexToByteArray()
|
||||
)
|
||||
logger.d("SealJSON: $sealJSON")
|
||||
|
||||
null
|
||||
}
|
||||
if (!recipientPublicKey.equals(keyPair.pubKey.toHexKey(), ignoreCase = true)) {
|
||||
// Not ours to open, and no key we hold ever will be: the wrap is
|
||||
// encrypted to the recipient with a one-off key that
|
||||
// GiftWrapEvent.create() discards, so not even the sender can
|
||||
// unwrap their own gift wrap.
|
||||
logger.d("GiftWrap $id is addressed to $recipientPublicKey, not to us")
|
||||
} else {
|
||||
val nostrSigner = NostrSignerInternal(
|
||||
keyPair = KeyPair(privKey = keyPair.privKey)
|
||||
|
||||
Reference in New Issue
Block a user