diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index aa4a0205ad..d36bdc194e 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -1688,22 +1688,26 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag User {userChatRelay} = user -- TODO [relays] owner, relays: TBC how to communicate member rejection rules from owner to relays memberJoinRequestViaRelay :: InvitationId -> VersionRangeChat -> Maybe SignedMsg -> Profile -> MemberId -> MemberKey -> Maybe MemberId -> CM () - memberJoinRequestViaRelay invId chatVRange signedMsg_ p joiningMemberId joiningMemberKey@(MemberKey joiningKey) viaRelay = do + memberJoinRequestViaRelay invId chatVRange signedMsg_ p@Profile {displayName} joiningMemberId joiningMemberKey@(MemberKey joiningKey) viaRelay = do (_ucl, gLinkInfo_) <- withStore $ \db -> getUserContactLinkById db userId uclId case gLinkInfo_ of Just GroupLinkInfo {groupId, memberRole = gLinkMemRole} -> do g@(GIK gInfo _) <- withStore $ \db -> getGroupInfoKeys db cxt user groupId - existing_ <- withStore' $ \db -> eitherToMaybe <$> runExceptT (getGroupMemberByMemberId db cxt user gInfo joiningMemberId) - let signed = memberSigned gInfo joiningMemberId joiningKey signedMsg_ - case existing_ of - Just rosterMem - -- a privileged memberId's key is owner-authoritative (the roster); the joiner must prove - -- possession of that exact key, otherwise this is an attempt to impersonate it - | isRosterRole (memberRole' rosterMem) -> - if verifyKey gInfo rosterMem signed - then acceptJoin g signed (Just rosterMem) (memberRole' rosterMem) - else messageError "memberJoinRequestViaRelay: rejected join claiming privileged memberId (key mismatch or invalid signature)" - _ -> acceptJoin g signed Nothing gLinkMemRole + if + | not (useRelays' gInfo) -> + messageWarning $ "memberJoinRequestViaRelay (group " <> groupName' gInfo <> "): ignored relay join request from " <> displayName <> " (group does not use relays)" + | otherwise -> do + existing_ <- withStore' $ \db -> eitherToMaybe <$> runExceptT (getGroupMemberByMemberId db cxt user gInfo joiningMemberId) + let signed = memberSigned gInfo joiningMemberId joiningKey signedMsg_ + case existing_ of + Just rosterMem + -- a privileged memberId's key is owner-authoritative (the roster); the joiner must prove + -- possession of that exact key, otherwise this is an attempt to impersonate it + | isRosterRole (memberRole' rosterMem) -> + if verifyKey gInfo rosterMem signed + then acceptJoin g signed (Just rosterMem) (memberRole' rosterMem) + else messageError "memberJoinRequestViaRelay: rejected join claiming privileged memberId (key mismatch or invalid signature)" + _ -> acceptJoin g signed Nothing gLinkMemRole Nothing -> messageError "memberJoinRequestViaRelay: no group link info for relay link" where diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 01a504f13c..136e417ef2 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -39,7 +39,7 @@ import Simplex.Chat.Messages (CIMention (..), CIMentionMember (..), ChatItemId) import Simplex.Chat.Messages.Batch (encodeBinaryBatch, encodeFwdElement) import Simplex.Chat.Messages.CIContent (publicGroupNoE2EText) import Simplex.Chat.Options -import Simplex.Chat.Protocol (ChatMessage (ChatMessage), ChatMsgEvent (XGrpMemNew, XGrpRelayInv, XInfo, XMsgUpdate, XMsgNew, XMsgDel), FwdSender (FwdMember, FwdChannel), GrpMsgForward (GrpMsgForward), MsgContainer (..), MsgMention (..), MsgContent (..), VerifiedMsg (VMUnsigned), chatMsgToBody, mcSimple, msgContentText) +import Simplex.Chat.Protocol (ChatMessage (ChatMessage), ChatMsgEvent (XGrpMemNew, XGrpRelayInv, XInfo, XMember, XMsgUpdate, XMsgNew, XMsgDel), FwdSender (FwdMember, FwdChannel), GrpMsgForward (GrpMsgForward), MsgContainer (..), MsgMention (..), MsgContent (..), VerifiedMsg (VMUnsigned), chatMsgToBody, mcSimple, msgContentText) import Simplex.Chat.Types import Simplex.Chat.Types.MemberRelations (MemberRelation (..), getRelation, setRelation) import Simplex.Chat.Types.Shared (GroupMemberRole (..), GroupAcceptance (..)) @@ -135,6 +135,7 @@ chatGroupTests = do it "demotion does not remove group link" testGroupLinkDemotedAdmin it "host profile received" testGroupLinkHostProfileReceived it "existing contact merged" testGroupLinkExistingContactMerged + it "ignore relay join request in group without relays" testGroupLinkIgnoreRelayJoinRequest describe "group links - member screening" $ do it "reject member - blocked name" testGLinkRejectBlockedName it "accept member - only host approval" testGLinkApproveMember @@ -3629,6 +3630,32 @@ testGroupLinkExistingContactMerged = bob #> "#team hi there" alice <# "#team bob> hi there" +testGroupLinkIgnoreRelayJoinRequest :: HasCallStack => TestParams -> IO () +testGroupLinkIgnoreRelayJoinRequest ps = + withNewTestChatOpts ps (testOpts {coreOptions = testCoreOpts {logLevel = CLLWarning}}) "alice" aliceProfile $ \alice -> + withNewTestChat ps "bob" bobProfile $ \bob -> do + alice ##> "/g team" + alice <## "group #team is created" + alice <## "to add members use /a team or /create link #team" + alice ##> "/create link #team" + (gShortLink, _gLink) <- getGroupLinks alice "team" GRMember True + sendMemberJoinRequest bob gShortLink + alice <## "warning: memberJoinRequestViaRelay (group team): ignored relay join request from bob (group does not use relays)" + alice ##> "/ms team" + alice <## "alice (Alice): owner, you, created group" + +sendMemberJoinRequest :: TestCC -> String -> IO () +sendMemberJoinRequest cc groupLink = + withCCUser cc $ \user -> do + gLink :: ShortLinkContact <- either fail pure $ strDecode (B.pack groupLink) + (memberKey, _) <- atomically . C.generateKeyPair =<< C.newRandom + let joinGroup = do + (_, _, cReq) <- getShortLinkConnReq NRMInteractive user gLink + (connId, _) <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq PQSupportOff + dm <- encodeConnInfo $ XMember bobProfile (MemberId "member") (MemberKey memberKey) Nothing + withAgent $ \a -> joinConnection a NRMInteractive (aUserId user) connId True cReq dm PQSupportOff SMSubscribe + runExceptT joinGroup `runReaderT` chatController cc >>= either (fail . show) (\_ -> pure ()) + testGLinkRejectBlockedName :: HasCallStack => TestParams -> IO () testGLinkRejectBlockedName = testChatCfg2 cfg aliceProfile bobProfile $