fix(nostr): cap DM envelope backdating at 24 hours

This commit is contained in:
wollow 2026-08-11 14:52:16 +03:00
parent fcb4562bd5
commit 76bf226f98
4 changed files with 55 additions and 4 deletions

View File

@ -317,9 +317,9 @@ object NostrCrypto {
}
/**
* Random timestamp up to maxPastSeconds in the past (default 2 days)
* Random timestamp up to maxPastSeconds in the past (default 24 hours for iOS compatibility)
*/
fun randomizeTimestampUpToPast(maxPastSeconds: Int = 172800): Int {
fun randomizeTimestampUpToPast(maxPastSeconds: Int = 86400): Int {
val now = (System.currentTimeMillis() / 1000).toInt()
val offset = if (maxPastSeconds > 0) secureRandom.nextInt(maxPastSeconds + 1) else 0
return now - offset

View File

@ -37,7 +37,7 @@ 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 iOS's 24h lookback
val sealedEvent = createSeal(
rumor = rumor,
recipientPubkey = recipientPubkey,

View File

@ -3,6 +3,7 @@ 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 {
@ -41,6 +42,47 @@ class NostrProtocolTest {
assertNull(decrypted)
}
@Test
fun createPrivateMessage_limitsEnvelopeTimestampsToIosLookback() {
val sender = NostrIdentity.generate()
val recipient = NostrIdentity.generate()
repeat(20) {
val beforeCreation = (System.currentTimeMillis() / 1000).toInt()
val giftWrap = NostrProtocol.createPrivateMessage(
content = "bitchat1:test",
recipientPubkey = recipient.publicKeyHex,
senderIdentity = sender
).single()
val afterCreation = (System.currentTimeMillis() / 1000).toInt()
val sealJson = NostrCrypto.decryptNIP44(
ciphertext = giftWrap.content,
senderPublicKeyHex = giftWrap.pubkey,
recipientPrivateKeyHex = recipient.privateKeyHex
)
val seal = gson.fromJson(sealJson, NostrEvent::class.java)
assertTimestampWithinIosLookback("gift wrap", giftWrap.createdAt, beforeCreation, afterCreation)
assertTimestampWithinIosLookback("seal", seal.createdAt, beforeCreation, afterCreation)
}
}
private fun assertTimestampWithinIosLookback(
envelope: String,
createdAt: Int,
beforeCreation: Int,
afterCreation: Int
) {
assertTrue(
"$envelope timestamp must be no more than 24 hours old",
createdAt >= beforeCreation - IOS_DM_LOOKBACK_SECONDS
)
assertTrue(
"$envelope timestamp must not be in the future",
createdAt <= afterCreation
)
}
private fun forgedGiftWrap(
content: String,
claimedSender: NostrIdentity,
@ -82,4 +124,8 @@ class NostrProtocolTest {
content = giftWrapContent
).sign(wrapPrivateKey)
}
private companion object {
const val IOS_DM_LOOKBACK_SECONDS = 86400
}
}

View File

@ -17,7 +17,7 @@ The remaining implementation work and milestone progress are tracked in
| Inner payloads | Noise type bytes, private-message TLVs, peer-state TLVs, file-transfer TLVs, live-voice bursts, fragment header, sync request TLVs | `ClientRewriteWireContractTest`, `AuthenticatedPeerStateTest`, `PrivateMediaTransferPreparerTest`, `VoiceBurstPacketTest`, `FragmentManagerTest` |
| Identity/security | Announcement extensions, capability bitfield endianness, Noise static-key binding, handshake identity binding, signatures | `IdentityAnnouncementTest`, `NoiseSessionManagerIdentityBindingTest`, `ClientRewritePrimitiveContractTest` |
| Sync/routing | Stable packet IDs, GCS bitstream, replay collapse, TTL handling, relay choice, confirmed graph edges | `ClientRewritePrimitiveContractTest`, `GCSFilterTest`, `PacketRelayManagerTest`, `MeshGraphServiceTest`, `TransportBridgeServiceTest` |
| Nostr | Bech32, secp256k1 key derivation, NIP-01 event IDs/signatures, NIP-44 authenticated encryption, NIP-13 PoW, authenticated NIP-17 seals | `ClientRewriteNostrContractTest`, `NostrProtocolTest` |
| Nostr | Bech32, secp256k1 key derivation, NIP-01 event IDs/signatures, NIP-44 authenticated encryption, NIP-13 PoW, authenticated NIP-17 seals, 24-hour outbound envelope randomization | `ClientRewriteNostrContractTest`, `NostrProtocolTest` |
| Application state | Peer unions, canonical private conversations, chronological history, delivery/read behavior, media migration policy | `AppStateStoreTest`, `PrivateChatManagerTest`, `MediaSendingManagerMigrationTest` |
## Golden-vector policy
@ -31,6 +31,11 @@ Round-trip tests remain useful but are not sufficient on their own: an encoder
and decoder can share the same defect. Each critical wire format therefore has
at least one literal vector.
NIP-17 receivers must use a lookback at least as long as the maximum timestamp
randomization used by senders. Android caps outbound seal and gift-wrap
randomization at 24 hours for iOS interoperability while retaining its 48-hour
receive lookback.
## Rewrite acceptance gate
From a configured Android development environment, run: