diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 95d7805112..4c0de89be2 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -2961,7 +2961,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = _ -> pure (conn', Nothing) xGrpMemNew :: GroupInfo -> GroupMember -> MemberInfo -> Maybe MsgScope -> RcvMessage -> UTCTime -> CM (Maybe DeliveryJobScope) - xGrpMemNew gInfo m memInfo@(MemberInfo memId memRole _ _ _) msgScope_ msg brokerTs = do + xGrpMemNew gInfo m memInfo@(MemberInfo memId memRole _ _ assertedKey_) msgScope_ msg brokerTs = do let fromRelay = useRelays' gInfo && isRelay m unless fromRelay $ checkHostRole m memRole if sameMemberId memId (membership gInfo) @@ -2973,6 +2973,13 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- never the role or key (those are owner-authoritative via the roster, and -- XGrpMemNew is unsigned) | fromRelay && isRosterRole (memberRole' unknownMember) -> do + -- a member's key is immutable per memberId and identical across relays; mismatch + -- is unambiguous relay misbehavior (role can legitimately differ across relays + -- under multi-relay skew, so we deliberately don't warn on role) + let assertedKey = (\(MemberKey k) -> k) <$> assertedKey_ + -- TODO [relays] member: surface relay-key-mismatch as a dedicated event / chat item / relay state + when (assertedKey /= memberPubKey unknownMember) $ + messageWarning $ "x.grp.mem.new: relay asserted key differs from roster-established key, keeping roster key, memberId=" <> safeDecodeUtf8 (strEncode memId) updatedMember <- withStore $ \db -> updateRosterMemberAnnounced db vr user m unknownMember memInfo initialStatus -- roster members can't be pending, so no members-require-attention update gInfo' <- updatePublicGroupData user gInfo diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 2a180660c5..daf094fed6 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -26,7 +26,7 @@ import Data.Int (Int64) import Data.List (intercalate, isInfixOf) import qualified Data.Map.Strict as M import qualified Data.Text as T -import Simplex.Chat.Controller (ChatConfig (..), ChatHooks (..), defaultChatHooks) +import Simplex.Chat.Controller (ChatConfig (..), ChatHooks (..), ChatLogLevel (..), defaultChatHooks) import Simplex.Chat.Library.Internal (uniqueMsgMentions, updatedMentionNames) import Simplex.Chat.Markdown (parseMaybeMarkdownList) import Simplex.Chat.Messages (CIMention (..), CIMentionMember (..), ChatItemId) @@ -9698,7 +9698,7 @@ testChannelRelayCannotDowngradeRosterMember ps = withNewTestChat ps "alice" aliceProfile $ \alice -> withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> withNewTestChat ps "cath" cathProfile $ \cath -> - withNewTestChat ps "frank" frankProfile $ \frank -> do + withNewTestChatOpts ps (testOpts {coreOptions = testCoreOpts {logLevel = CLLWarning}}) "frank" frankProfile $ \frank -> do (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob memberJoinChannel "team" [bob] [alice] shortLink fullLink cath memberJoinChannel "team" [bob] [alice] shortLink fullLink frank @@ -9727,17 +9727,17 @@ testChannelRelayCannotDowngradeRosterMember ps = concurrentlyN_ [ alice <# "#team cath> hello from cath [>>]", do + frank <##. "warning: x.grp.mem.new: relay asserted key differs from roster-established key, keeping roster key, memberId=" frank <## "#team: unknown member cath updated to cath" frank <## "#team: bob introduced cath (Catherine) in the channel" frank <# "#team cath> hello from cath [>>]" ] threadDelay 100000 - -- with the fix, frank's row for cath retains the roster-established role and key checkMemberRow frank "cath" (Just "moderator") frankKey <- getMemberPubKey frank "cath" frankKey `shouldBe` realKey where - getMemberPubKey :: HasCallStack => TestCC -> T.Text -> IO (Maybe ByteString) + getMemberPubKey :: TestCC -> T.Text -> IO (Maybe ByteString) getMemberPubKey cc name = do rows <- withCCTransaction cc $ \db -> DB.query db "SELECT member_pub_key FROM group_members WHERE local_display_name = ?" (Only name) :: IO [Only (Maybe ByteString)]