diff --git a/app/src/main/java/com/bitchat/android/nostr/NostrProtocol.kt b/app/src/main/java/com/bitchat/android/nostr/NostrProtocol.kt
index 4376d1e9..5682a365 100644
--- a/app/src/main/java/com/bitchat/android/nostr/NostrProtocol.kt
+++ b/app/src/main/java/com/bitchat/android/nostr/NostrProtocol.kt
@@ -13,6 +13,7 @@ import kotlinx.coroutines.withContext
object NostrProtocol {
private const val TAG = "NostrProtocol"
+ private const val NIP17_IOS_COMPATIBLE_LOOKBACK_SECONDS = 86_400
private val gson = Gson()
/**
@@ -37,7 +38,8 @@ object NostrProtocol {
val rumorId = rumorBase.computeEventIdHex()
val rumor = rumorBase.copy(id = rumorId)
- // 2. Seal the rumor (kind 13) signed by sender, timestamp randomized up to 2 days
+ // 2. Seal the rumor (kind 13) signed by sender, timestamp randomized within the
+ // receiver subscription window used by iOS.
val sealedEvent = createSeal(
rumor = rumor,
recipientPubkey = recipientPubkey,
@@ -231,7 +233,9 @@ object NostrProtocol {
val seal = NostrEvent(
pubkey = senderPublicKey,
- createdAt = NostrCrypto.randomizeTimestampUpToPast(),
+ createdAt = NostrCrypto.randomizeTimestampUpToPast(
+ NIP17_IOS_COMPATIBLE_LOOKBACK_SECONDS
+ ),
kind = NostrKind.SEAL,
tags = emptyList(),
content = encrypted
@@ -260,7 +264,9 @@ object NostrProtocol {
val giftWrap = NostrEvent(
pubkey = wrapPublicKey,
- createdAt = NostrCrypto.randomizeTimestampUpToPast(),
+ createdAt = NostrCrypto.randomizeTimestampUpToPast(
+ NIP17_IOS_COMPATIBLE_LOOKBACK_SECONDS
+ ),
kind = NostrKind.GIFT_WRAP,
tags = listOf(listOf("p", recipientPubkey)), // Tag recipient
content = encrypted
diff --git a/app/src/test/kotlin/com/bitchat/android/nostr/NostrProtocolTest.kt b/app/src/test/kotlin/com/bitchat/android/nostr/NostrProtocolTest.kt
index a5bd9561..fa454b3b 100644
--- a/app/src/test/kotlin/com/bitchat/android/nostr/NostrProtocolTest.kt
+++ b/app/src/test/kotlin/com/bitchat/android/nostr/NostrProtocolTest.kt
@@ -3,10 +3,12 @@ package com.bitchat.android.nostr
import com.google.gson.Gson
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
import org.junit.Test
class NostrProtocolTest {
private val gson = Gson()
+ private val nip17IosCompatibleLookbackSeconds = 86_400
@Test
fun decryptPrivateMessage_acceptsAuthenticatedSeal() {
@@ -41,6 +43,23 @@ class NostrProtocolTest {
assertNull(decrypted)
}
+ @Test
+ fun createPrivateMessage_keepsGiftWrapTimestampInsideIosLookbackWindow() {
+ val sender = NostrIdentity.generate()
+ val recipient = NostrIdentity.generate()
+ val before = (System.currentTimeMillis() / 1000).toInt()
+
+ val giftWrap = NostrProtocol.createPrivateMessage(
+ content = "bitchat1:timestamp-window",
+ recipientPubkey = recipient.publicKeyHex,
+ senderIdentity = sender
+ ).single()
+ val after = (System.currentTimeMillis() / 1000).toInt()
+
+ assertTrue(giftWrap.createdAt <= after)
+ assertTrue(giftWrap.createdAt >= before - nip17IosCompatibleLookbackSeconds)
+ }
+
private fun forgedGiftWrap(
content: String,
claimedSender: NostrIdentity,
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index a62cbff6..8d9b2acb 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -2147,6 +2147,9 @@
+
+
+