mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-18 18:16:53 +00:00
wip
This commit is contained in:
@@ -736,7 +736,7 @@ processChatCommand cxt nm = \case
|
||||
-- TODO [knocking] check chat item scope?
|
||||
cci <- withFastStore $ \db -> getGroupCIWithReactions db user gInfo itemId
|
||||
case cci of
|
||||
CChatItem SMDSnd ci@ChatItem {meta = CIMeta {itemSharedMsgId, itemTimed, itemLive, editable, showGroupAsSender}, content = ciContent} -> do
|
||||
CChatItem SMDSnd ci@ChatItem {meta = CIMeta {itemSharedMsgId, itemTimed, itemLive, editable, showGroupAsSender, msgSigned}, content = ciContent} -> do
|
||||
case (ciContent, itemSharedMsgId, editable) of
|
||||
(CISndMsgContent oldMC, Just itemSharedMId, True) -> do
|
||||
chatScopeInfo <- mapM (getChatScopeInfo cxt user) scope
|
||||
@@ -748,7 +748,8 @@ processChatCommand cxt nm = \case
|
||||
let msgScope = toMsgScope gInfo <$> chatScopeInfo
|
||||
mentions' = M.map (\CIMention {memberId} -> MsgMention {memberId}) ciMentions
|
||||
event = XMsgUpdate itemSharedMId mc mentions' (ttl' <$> itemTimed) (justTrue . (live &&) =<< itemLive) msgScope (Just showGroupAsSender)
|
||||
SndMessage {msgId} <- sendGroupMessage user gInfo scope recipients False event
|
||||
reuseSign = isJust msgSigned
|
||||
SndMessage {msgId} <- sendGroupMessage user gInfo scope recipients reuseSign event
|
||||
ci' <- withFastStore' $ \db -> do
|
||||
currentTs <- liftIO getCurrentTime
|
||||
when changed $
|
||||
@@ -5108,7 +5109,7 @@ chatCommandP =
|
||||
"/_get content types " *> (APIGetChatContentTypes <$> chatRefP),
|
||||
"/_get items " *> (APIGetChatItems <$> chatPaginationP <*> optional (" search=" *> textP)),
|
||||
"/_get item info " *> (APIGetChatItemInfo <$> chatRefP <* A.space <*> A.decimal),
|
||||
"/_send " *> (APISendMessages <$> sendRefP <*> liveMessageP <*> sendMessageTTLP <*> pure False <*> (" json " *> jsonP <|> " text " *> composedMessagesTextP)),
|
||||
"/_send " *> (APISendMessages <$> sendRefP <*> liveMessageP <*> sendMessageTTLP <*> signMessagesP <*> (" json " *> jsonP <|> " text " *> composedMessagesTextP)),
|
||||
"/_create tag " *> (APICreateChatTag <$> jsonP),
|
||||
"/_tags " *> (APISetChatTags <$> chatRefP <*> optional _strP),
|
||||
"/_delete tag " *> (APIDeleteChatTag <$> A.decimal),
|
||||
@@ -5482,6 +5483,7 @@ chatCommandP =
|
||||
pure [composedMessage Nothing text]
|
||||
updatedMessagesTextP = (`UpdatedMessage` []) <$> mcTextP
|
||||
liveMessageP = " live=" *> onOffP <|> pure False
|
||||
signMessagesP = " sign=" *> onOffP <|> pure False
|
||||
sendMessageTTLP = " ttl=" *> ((Just <$> A.decimal) <|> ("default" $> Nothing)) <|> pure Nothing
|
||||
receiptSettings = do
|
||||
enable <- onOffP
|
||||
|
||||
@@ -2223,7 +2223,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
groupMsgToView cInfo ci' {reactions}
|
||||
|
||||
groupMessageUpdate :: GroupInfo -> Maybe GroupMember -> SharedMsgId -> MsgContent -> Map MemberName MsgMention -> Maybe MsgScope -> RcvMessage -> UTCTime -> Maybe Int -> Maybe Bool -> Maybe Bool -> CM (Maybe DeliveryTaskContext)
|
||||
groupMessageUpdate gInfo@GroupInfo {groupId} m_ sharedMsgId mc mentions msgScope_ msg@RcvMessage {msgId} brokerTs ttl_ live_ asGroup_
|
||||
groupMessageUpdate gInfo@GroupInfo {groupId} m_ sharedMsgId mc mentions msgScope_ msg@RcvMessage {msgId, msgSigned} brokerTs ttl_ live_ asGroup_
|
||||
| Just m <- m_, prohibitedSimplexLinks gInfo m mc ft_ =
|
||||
messageWarning ("x.msg.update ignored: feature not allowed " <> groupFeatureNameText GFSimplexLinks) $> Nothing
|
||||
| otherwise = do
|
||||
@@ -2275,15 +2275,22 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
Nothing -> getGroupChatItemBySharedMsgId db user gInfo Nothing sharedMsgId
|
||||
(cci,) <$> getGroupChatScopeInfoForItem db cxt user gInfo (cChatItemId cci)
|
||||
case cci of
|
||||
CChatItem SMDRcv ci@ChatItem {chatDir = CIGroupRcv m', meta = CIMeta {itemLive}, content = CIRcvMsgContent oldMC}
|
||||
| isSender m' -> updateCI False ci scopeInfo oldMC itemLive (Just $ memberId' m')
|
||||
CChatItem SMDRcv ci@ChatItem {chatDir = CIGroupRcv m', meta = CIMeta {itemLive, msgSigned = itemSigned}, content = CIRcvMsgContent oldMC}
|
||||
| isSender m' -> requireSignedMutation (CDGroupRcv gInfo scopeInfo m') itemSigned $ updateCI False ci scopeInfo oldMC itemLive (Just $ memberId' m')
|
||||
| otherwise -> messageError "x.msg.update: group member attempted to update a message of another member" $> Nothing
|
||||
CChatItem SMDRcv ci@ChatItem {chatDir = CIChannelRcv, meta = CIMeta {itemLive}, content = CIRcvMsgContent oldMC}
|
||||
| maybe True (\m -> memberRole' m == GROwner) m_ -> updateCI True ci scopeInfo oldMC itemLive Nothing
|
||||
CChatItem SMDRcv ci@ChatItem {chatDir = CIChannelRcv, meta = CIMeta {itemLive, msgSigned = itemSigned}, content = CIRcvMsgContent oldMC}
|
||||
| maybe True (\m -> memberRole' m == GROwner) m_ -> requireSignedMutation (CDChannelRcv gInfo scopeInfo) itemSigned $ updateCI True ci scopeInfo oldMC itemLive Nothing
|
||||
| otherwise -> messageError "x.msg.update: member attempted to update channel message" $> Nothing
|
||||
_ -> messageError "x.msg.update: invalid message update" $> Nothing
|
||||
where
|
||||
isSender m' = maybe False (\m -> sameMemberId (memberId' m) m') m_
|
||||
-- reject an unsigned XMsgUpdate of a held-signed item (edit-downgrade spoof) fail-closed, mirroring withVerifiedMsg
|
||||
requireSignedMutation :: ChatDirection 'CTGroup 'MDRcv -> Maybe MsgSigStatus -> CM (Maybe DeliveryTaskContext) -> CM (Maybe DeliveryTaskContext)
|
||||
requireSignedMutation cd itemSigned action
|
||||
| isJust itemSigned && isNothing msgSigned = do
|
||||
createInternalChatItem user cd (CIRcvGroupEvent RGEMsgBadSignature) (Just brokerTs)
|
||||
pure Nothing
|
||||
| otherwise = action
|
||||
updateCI :: ShowGroupAsSender -> ChatItem 'CTGroup 'MDRcv -> Maybe GroupChatScopeInfo -> MsgContent -> Maybe Bool -> Maybe MemberId -> CM (Maybe DeliveryTaskContext)
|
||||
updateCI showGroupAsSender ci scopeInfo oldMC itemLive memberId = do
|
||||
let changed = mc /= oldMC
|
||||
|
||||
+133
-1
@@ -36,7 +36,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), FwdSender (FwdMember), GrpMsgForward (GrpMsgForward), MsgMention (..), MsgContent (..), VerifiedMsg (VMUnsigned), msgContentText)
|
||||
import Simplex.Chat.Protocol (ChatMessage (ChatMessage), ChatMsgEvent (XGrpMemNew, XMsgUpdate), FwdSender (FwdMember), GrpMsgForward (GrpMsgForward), MsgMention (..), MsgContent (..), VerifiedMsg (VMUnsigned), msgContentText)
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Types.MemberRelations (MemberRelation (..), getRelation, setRelation)
|
||||
import Simplex.Chat.Types.Shared (GroupMemberRole (..), GroupAcceptance (..))
|
||||
@@ -330,6 +330,8 @@ chatGroupTests = do
|
||||
it "should compute sendAsGroup in CLI forward" testForwardCLISendAsGroup
|
||||
it "should update member message in channel" testChannelMemberMessageUpdate
|
||||
it "should delete member message in channel" testChannelMemberMessageDelete
|
||||
it "should sign member message and reuse signature on edit" testChannelMemberMessageSign
|
||||
it "should reject unsigned update of a signed item" testChannelMemberUpdateEnforcement
|
||||
|
||||
testGroupCheckMessages :: HasCallStack => TestParams -> IO ()
|
||||
testGroupCheckMessages =
|
||||
@@ -11952,6 +11954,136 @@ testChannelMemberMessageDelete ps =
|
||||
eve <# "#team cath> [marked deleted] hello"
|
||||
]
|
||||
|
||||
testChannelMemberMessageSign :: HasCallStack => TestParams -> IO ()
|
||||
testChannelMemberMessageSign ps =
|
||||
withNewTestChat ps "alice" aliceProfile $ \alice ->
|
||||
withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob ->
|
||||
withNewTestChat ps "cath" cathProfile $ \cath ->
|
||||
withNewTestChat ps "dan" danProfile $ \dan ->
|
||||
withNewTestChat ps "eve" eveProfile $ \eve -> do
|
||||
createChannel1Relay "team" alice bob cath dan eve
|
||||
promoteChannelMember "team" alice bob cath [dan, eve]
|
||||
|
||||
-- member sends a signed message
|
||||
cath ##> "/_send #1 sign=on text signed hello"
|
||||
cath <# "#team signed hello"
|
||||
bob <# "#team cath> signed hello"
|
||||
concurrentlyN_
|
||||
[ alice <# "#team cath> signed hello [>>]",
|
||||
do dan <### [EndsWith "updated to cath"]
|
||||
dan <## "#team: bob introduced cath (Catherine) in the channel"
|
||||
dan <# "#team cath> signed hello [>>]",
|
||||
do eve <### [EndsWith "updated to cath"]
|
||||
eve <## "#team: bob introduced cath (Catherine) in the channel"
|
||||
eve <# "#team cath> signed hello [>>]"
|
||||
]
|
||||
-- sender and recipient hold it signed
|
||||
cath #$> ("/_get chat #1 count=1", chat, [(1, "signed hello (signed)")])
|
||||
dan #$> ("/_get chat #1 count=1", chat, [(0, "signed hello (signed)")])
|
||||
|
||||
-- editing a signed item reuses the signature
|
||||
cathMsgId <- lastItemId cath
|
||||
cath ##> ("/_update item #1 " <> cathMsgId <> " text signed hello edited")
|
||||
cath <# "#team [edited] signed hello edited"
|
||||
bob <# "#team cath> [edited] signed hello edited"
|
||||
concurrentlyN_
|
||||
[ alice <# "#team cath> [edited] signed hello edited",
|
||||
dan <# "#team cath> [edited] signed hello edited",
|
||||
eve <# "#team cath> [edited] signed hello edited"
|
||||
]
|
||||
cath #$> ("/_get chat #1 count=1", chat, [(1, "signed hello edited (signed)")])
|
||||
dan #$> ("/_get chat #1 count=1", chat, [(0, "signed hello edited (signed)")])
|
||||
|
||||
-- default send is unsigned, and holds no signature
|
||||
cath #> "#team plain hello"
|
||||
bob <# "#team cath> plain hello"
|
||||
concurrentlyN_
|
||||
[ alice <# "#team cath> plain hello [>>]",
|
||||
dan <# "#team cath> plain hello [>>]",
|
||||
eve <# "#team cath> plain hello [>>]"
|
||||
]
|
||||
cath #$> ("/_get chat #1 count=1", chat, [(1, "plain hello")])
|
||||
dan #$> ("/_get chat #1 count=1", chat, [(0, "plain hello")])
|
||||
|
||||
testChannelMemberUpdateEnforcement :: HasCallStack => TestParams -> IO ()
|
||||
testChannelMemberUpdateEnforcement ps =
|
||||
withNewTestChat ps "alice" aliceProfile $ \alice ->
|
||||
withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob ->
|
||||
withNewTestChat ps "cath" cathProfile $ \cath ->
|
||||
withNewTestChat ps "dan" danProfile $ \dan ->
|
||||
withNewTestChat ps "eve" eveProfile $ \eve -> do
|
||||
createChannel1Relay "team" alice bob cath dan eve
|
||||
promoteChannelMember "team" alice bob cath [dan, eve]
|
||||
|
||||
-- cath posts a signed message; dan holds it verified
|
||||
cath ##> "/_send #1 sign=on text secret"
|
||||
cath <# "#team secret"
|
||||
bob <# "#team cath> secret"
|
||||
concurrentlyN_
|
||||
[ alice <# "#team cath> secret [>>]",
|
||||
do dan <### [EndsWith "updated to cath"]
|
||||
dan <## "#team: bob introduced cath (Catherine) in the channel"
|
||||
dan <# "#team cath> secret [>>]",
|
||||
do eve <### [EndsWith "updated to cath"]
|
||||
eve <## "#team: bob introduced cath (Catherine) in the channel"
|
||||
eve <# "#team cath> secret [>>]"
|
||||
]
|
||||
dan #$> ("/_get chat #1 count=1", chat, [(0, "secret (signed)")])
|
||||
|
||||
-- the malicious relay forges an unsigned XMsgUpdate of cath's signed item to dan
|
||||
cathMemId <- memberIdByName bob "cath"
|
||||
sharedId <- itemSharedMsgId cath
|
||||
connId <- relayConnIdToMember bob "dan"
|
||||
ts <- getCurrentTime
|
||||
let ChatController {smpAgent = bobAgent} = chatController bob
|
||||
chatMsg = ChatMessage chatInitialVRange Nothing (XMsgUpdate sharedId (MCText "forged") M.empty Nothing Nothing Nothing Nothing)
|
||||
fwd = GrpMsgForward (FwdMember cathMemId "cath") ts
|
||||
body = encodeBinaryBatch [encodeFwdElement fwd (VMUnsigned chatMsg)]
|
||||
sent <- runExceptT $ sendMessages bobAgent [(connId, PQEncOff, MsgFlags False, vrValue body)]
|
||||
either (fail . show) (const $ pure ()) sent
|
||||
-- dan rejects the unsigned mutation of the held-signed item (RGEMsgBadSignature, stored not shown live),
|
||||
-- and the original signed content is not overwritten
|
||||
threadDelay 2000000
|
||||
dan #$> ("/_get chat #1 count=2", chat, [(0, "secret (signed)"), (0, "message rejected: bad signature")])
|
||||
|
||||
-- a legitimate signed edit by cath is accepted
|
||||
cathMsgId <- lastItemId cath
|
||||
cath ##> ("/_update item #1 " <> cathMsgId <> " text secret edited")
|
||||
cath <# "#team [edited] secret edited"
|
||||
bob <# "#team cath> [edited] secret edited"
|
||||
concurrentlyN_
|
||||
[ alice <# "#team cath> [edited] secret edited",
|
||||
dan <# "#team cath> [edited] secret edited",
|
||||
eve <# "#team cath> [edited] secret edited"
|
||||
]
|
||||
dan #$> ("/_get chat #1 count=2", chat, [(0, "secret edited (signed)"), (0, "message rejected: bad signature")])
|
||||
where
|
||||
memberIdByName :: TestCC -> T.Text -> IO MemberId
|
||||
memberIdByName cc name = do
|
||||
rows <- withCCTransaction cc $ \db ->
|
||||
DB.query db "SELECT member_id FROM group_members WHERE local_display_name = ?" (Only name) :: IO [Only ByteString]
|
||||
case rows of
|
||||
(Only mid : _) -> pure (MemberId mid)
|
||||
_ -> fail $ "no member " <> T.unpack name
|
||||
relayConnIdToMember :: TestCC -> T.Text -> IO ByteString
|
||||
relayConnIdToMember cc name = do
|
||||
rows <- withCCTransaction cc $ \db ->
|
||||
DB.query
|
||||
db
|
||||
"SELECT c.agent_conn_id FROM connections c JOIN group_members m ON m.group_member_id = c.group_member_id WHERE m.local_display_name = ?"
|
||||
(Only name) ::
|
||||
IO [Only ByteString]
|
||||
case rows of
|
||||
(Only connId : _) -> pure connId
|
||||
_ -> fail $ "no relay connection to member " <> T.unpack name
|
||||
itemSharedMsgId :: TestCC -> IO SharedMsgId
|
||||
itemSharedMsgId cc = do
|
||||
rows <- withCCTransaction cc $ \db ->
|
||||
DB.query_ db "SELECT shared_msg_id FROM chat_items WHERE shared_msg_id IS NOT NULL ORDER BY chat_item_id DESC LIMIT 1" :: IO [Only ByteString]
|
||||
case rows of
|
||||
(Only smid : _) -> pure (SharedMsgId smid)
|
||||
_ -> fail "no shared_msg_id"
|
||||
|
||||
testGroupLinkContentFilter :: HasCallStack => TestParams -> IO ()
|
||||
testGroupLinkContentFilter =
|
||||
testChat3 aliceProfile bobProfile cathProfile $
|
||||
|
||||
Reference in New Issue
Block a user