Skip to content

Prevent private chats with same-name peers from merging - #1736

Open
heyaim wants to merge 2 commits into
permissionlesstech:mainfrom
heyaim:prevent-same-name-private-chat-merging
Open

heyaim wants to merge 2 commits into
permissionlesstech:mainfrom
heyaim:prevent-same-name-private-chat-merging

Conversation

@heyaim

@heyaim heyaim commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Private chats with different peers can merge when their nicknames match. A reply or resent message intended for one peer can then be addressed to the other.

This happens locally on the device receiving their DMs; merging the chats does not itself send either peer the combined history.

The scenarios include:

  • If B's chat history is empty and either B's fingerprint or A's stored chat fingerprint is unavailable, an incoming DM from B can move or copy A's messages from the last 24 hours into B's chat when the nickname matches. A's stored fingerprint may be unavailable if the user hasn't opened that chat since launching the app.
  • While the combined chat is addressed to B, a reply meant for A can be addressed to B instead. Tapping Resend on a failed message meant for A from that chat can also send that message to B.
  • The combined chat can switch back: if all of A's history moved into B's chat, leaving A's chat empty, A's next DM can move the combined history back under A—including B's message—when either A's fingerprint or B's stored chat fingerprint is unavailable and the messages still meet the nickname and 24-hour conditions. The next reply from that chat can then be addressed to A.
  • The same problem can happen without the peers choosing the same nickname: couriered DMs whose senders' nicknames cannot be resolved are labeled "Unknown", so two of them can combine into one chat. This can still happen after opening the chat if a fingerprint needed to distinguish the peers remains unavailable.

Two code paths allowed this:

  • On mesh DM arrival, migratePrivateChatsIfNeeded could move or copy recent history into an empty conversation when either fingerprint was unavailable and a message matched the nickname. It checked the incoming fingerprint and the old chat's stored fingerprint, with a 24-hour cutoff.
  • On chat opening, PrivateChatManager.consolidateMessages could move a geohash DM thread and rewrite its sender IDs when every source row's sender name matched the peer's nickname, ignoring case. The upstream rule excludes threads containing this device's own rows, but a thread containing only the other sender's rows can still match by name. For a mesh peer, the match includes the geohash display-name suffix.

Changes

Updated after #1793 merged to preserve its fixes and focus this PR on the remaining nickname-only history joins between different peers.

  • Both nickname-based history rules and the geohash unread-name fallback were removed. Fingerprint matching and known Noise-key joins remain.
  • Full Noise-key courier arrivals now join their derived short-ID history without a nickname or age condition, using the short ID when connected and the full key otherwise.
  • Mesh row sender IDs are preserved for blocking and verification, and both receipt scans recognize the derived short alias. Existing Nostr callers keep their sender-ID rewrite.
  • Geohash DM Block actions now resolve through the sender's mapped Nostr key. The nickname fallback could block a matching mesh peer and leave the sender unblocked. If the mapping is missing, the action stops without blocking by name.

A shared name no longer joins history through these paths, including after a key change or between the same person's geohash and mesh identities.

Tests

  • Regression coverage includes history and unread separation, replies, Resend, same-key continuity, receipts, and Block targeting.
  • Tell my own messages apart by sender ID, not nickname #1793’s own-message and rename regressions were retained. Four overlapping tests from that change and the older GeoDM consolidation test were updated to require separation without identity proof. The older nickname-based unread expectation was removed, and test calls were adapted to the updated APIs.
  • The new A → B → A arrival regression checks that each peer’s history stays separate. The same sequence reproduced the history switch on upstream, with both a shared nickname and the label "Unknown". The "Unknown" case tests the label, not courier decryption or delivery.
  • A separate reply regression leaves B’s chat open and A’s fingerprint unavailable. When A sends another DM, the reply stays addressed to B. On upstream, the same sequence changed the open chat and reply destination to A.
  • Stable-key consolidation coverage checks that the device’s own rows retain its sender ID while the peer’s rows resolve to the peer’s routing ID.
  • Positive coverage for fingerprint matching and known Noise-key joins was preserved. The fingerprint-match fixture now resolves through the transport.
  • Temporarily restoring both upstream nickname joins, including the own-row guards, caused the remaining-case regressions to fail; restoring this candidate made them pass.

Validation

The history and reply tests reproduced the bug on upstream main at 04995f68 and passed with this fix. Deliberately restoring the faulty behavior made the relevant tests fail, covering nickname joins, sender identity, unread state, Block targeting, receipts and the retained authorship fixes.

Routing was checked through a mock transport; physical-device and courier delivery remain unverified.

Nickname matching could put one peer's messages in another peer's
private chat on DM arrival or chat opening. A reply or explicit Resend
could then target the wrong peer. GeoDM Block could also block a
matching mesh peer while leaving the sender unblocked.

Remove both history rules and the unread-name fallback. Keep fingerprint
and known-key joins. Join courier full-key and derived short-ID history,
preserving mesh sender IDs and recognizing the aliases for receipts.
Resolve geohash Block by the sender's mapped key; stop if it is missing.
Correct the fingerprint-test fixture to resolve through the transport.

Restoring the removed rules fails their regressions. App, package and
simulator tests, performance checks, SwiftLint, unsigned iOS simulator
and macOS Release builds, and macOS Periphery pass. iOS Periphery
matches the local baseline. No hardware run; receipts tested to
transport handoff.

@chessing234 chessing234 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed removal of nickname-based consolidation and unread matching, key-derived alias migration, sender-ID preservation, read-receipt alias checks, and identity-based blocking. No blocking issue found.

Validation limit: source review only, except the CSV validator noted above. The app/test suite was not built or run because this environment has Command Line Tools but no full Xcode installation.

Preserve the sender-ID authorship and receipt-sync fixes from permissionlesstech#1793
while retaining identity-based private chat separation.

Resolve overlapping tests so matching nicknames alone do not join
conversations. Keep the own-message and rename regressions, along
with fingerprint and known Noise-key continuity coverage.

Add a regression ensuring stable-key consolidation preserves the
device’s own sender ID while resolving the peer’s rows to its routing ID.

Cover repeated A-to-B-to-A arrivals and keep histories separate.
Verify that a reply from B’s open chat stays addressed to B when A
returns with the same name and an unresolved fingerprint.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants