mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-09-19 04:59:59 +00:00
Merge pull request #869 from qutad/fix/nostr-ios-dm-lookback
fix(nostr): cap DM envelope backdating at 24 hours
This commit is contained in:
commit
9444acd33e
@ -21,7 +21,9 @@ import java.math.BigInteger
|
||||
* Includes secp256k1 operations, ECDH, and NIP-44 encryption
|
||||
*/
|
||||
object NostrCrypto {
|
||||
|
||||
|
||||
internal const val NIP17_DEFAULT_MAX_PAST_SECONDS = 79_200
|
||||
|
||||
private val secureRandom = SecureRandom()
|
||||
// NIP-44 v2 only
|
||||
|
||||
@ -317,9 +319,9 @@ object NostrCrypto {
|
||||
}
|
||||
|
||||
/**
|
||||
* Random timestamp up to maxPastSeconds in the past (default 2 days)
|
||||
* Random timestamp in the past, defaulting to 22 hours to leave 2 hours of slack inside iOS's 24-hour lookback.
|
||||
*/
|
||||
fun randomizeTimestampUpToPast(maxPastSeconds: Int = 172800): Int {
|
||||
fun randomizeTimestampUpToPast(maxPastSeconds: Int = NIP17_DEFAULT_MAX_PAST_SECONDS): Int {
|
||||
val now = (System.currentTimeMillis() / 1000).toInt()
|
||||
val offset = if (maxPastSeconds > 0) secureRandom.nextInt(maxPastSeconds + 1) else 0
|
||||
return now - offset
|
||||
|
||||
@ -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 with 2 hours of slack inside iOS's 24-hour lookback.
|
||||
val sealedEvent = createSeal(
|
||||
rumor = rumor,
|
||||
recipientPubkey = recipientPubkey,
|
||||
|
||||
@ -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,52 @@ class NostrProtocolTest {
|
||||
assertNull(decrypted)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createPrivateMessage_reservesSlackInsideIosLookback() {
|
||||
val sender = NostrIdentity.generate()
|
||||
val recipient = NostrIdentity.generate()
|
||||
|
||||
assertEquals(
|
||||
IOS_DM_LOOKBACK_SECONDS - TIMESTAMP_SAFETY_SLACK_SECONDS,
|
||||
NostrCrypto.NIP17_DEFAULT_MAX_PAST_SECONDS
|
||||
)
|
||||
|
||||
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 leave 2 hours inside the iOS lookback",
|
||||
createdAt >= beforeCreation - MAX_OUTBOUND_BACKDATE_SECONDS
|
||||
)
|
||||
assertTrue(
|
||||
"$envelope timestamp must not be in the future",
|
||||
createdAt <= afterCreation
|
||||
)
|
||||
}
|
||||
|
||||
private fun forgedGiftWrap(
|
||||
content: String,
|
||||
claimedSender: NostrIdentity,
|
||||
@ -82,4 +129,11 @@ class NostrProtocolTest {
|
||||
content = giftWrapContent
|
||||
).sign(wrapPrivateKey)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val IOS_DM_LOOKBACK_SECONDS = 86_400
|
||||
const val TIMESTAMP_SAFETY_SLACK_SECONDS = 7_200
|
||||
const val MAX_OUTBOUND_BACKDATE_SECONDS =
|
||||
IOS_DM_LOOKBACK_SECONDS - TIMESTAMP_SAFETY_SLACK_SECONDS
|
||||
}
|
||||
}
|
||||
|
||||
@ -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, 22h 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 should reserve safety slack beyond the maximum timestamp
|
||||
randomization used by senders. Android caps outbound seal and gift-wrap
|
||||
randomization at 22h, leaving 2 hours of slack inside iOS's 24-hour
|
||||
subscription window, while retaining its 48-hour receive lookback.
|
||||
|
||||
## Rewrite acceptance gate
|
||||
|
||||
From a configured Android development environment, run:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user