From 74fe5340f7e0a95e2989cc17ad8f210705a4e8a6 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Wed, 18 Mar 2026 08:47:40 +0000 Subject: [PATCH] core: verify signed messages in channels (fix member keys not saved); sign deletion message for deleted member; relay member key (#6683) --- src/Simplex/Chat/Library/Commands.hs | 7 +- src/Simplex/Chat/Library/Internal.hs | 20 ++- src/Simplex/Chat/Library/Subscriber.hs | 83 +++++++----- src/Simplex/Chat/Protocol.hs | 8 +- src/Simplex/Chat/Store/Groups.hs | 102 ++++++++++++--- .../SQLite/Migrations/agent_query_plans.txt | 122 ++++++++++++++++++ .../SQLite/Migrations/chat_query_plans.txt | 30 ++++- tests/ChatTests/Groups.hs | 5 +- 8 files changed, 310 insertions(+), 67 deletions(-) diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 7487828f08..5aab0c5871 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -1999,7 +1999,7 @@ processChatCommand vr nm = \case sLnk <- case toShortLinkContact connLinkToConnect of Just sl -> pure sl Nothing -> throwChatError $ CEException "failed to retrieve relays: no short link" - (FixedLinkData {linkConnReq = mainCReq@(CRContactUri crData), linkEntityId, rootKey}, ContactLinkData _ UserContactData {relays}) <- getShortLinkConnReq nm user sLnk + (FixedLinkData {linkConnReq = mainCReq@(CRContactUri crData), linkEntityId, rootKey}, ContactLinkData _ UserContactData {owners, relays}) <- getShortLinkConnReq nm user sLnk -- Set group link info and incognito profile once before connecting to relays incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing let cReqHash = contactCReqHash $ CRContactUri crData {crScheme = SSSimplex} @@ -2008,6 +2008,9 @@ processChatCommand vr nm = \case gVar <- asks random (_, memberPrivKey) <- liftIO $ atomically $ C.generateKeyPair gVar withFastStore' $ \db -> updateGroupMemberKeys db groupId sharedGroupId rootKey memberPrivKey (groupMemberId' $ membership gInfo') + -- Pre-emptively create owner member with trusted key from link data + forM_ owners $ \OwnerAuth {ownerId, ownerKey} -> + withFastStore $ \db -> createLinkOwnerMember db vr user gInfo' (MemberId ownerId) ownerKey rs <- mapConcurrently (connectToRelay gInfo') relays let relayFailed = \case (_, _, Left _) -> True; _ -> False (failed, succeeded) = partition relayFailed rs @@ -3421,7 +3424,7 @@ processChatCommand vr nm = \case Just (Just gInfo) | useRelays' gInfo -> do let GroupInfo {membership = GroupMember {memberId}} = gInfo (memberPubKey, _memberPrivKey) <- atomically $ C.generateKeyPair g - -- TODO: store memberPrivKey in groups.member_priv_key, memberPubKey in group_members.member_pub_key + -- TODO [member keys] store memberPrivKey in groups.member_priv_key, memberPubKey in group_members.member_pub_key pure $ XMember profileToSend memberId (MemberKey memberPubKey) _ -> pure $ XContact profileToSend (Just xContactId) welcomeSharedMsgId msg_ dm <- encodeConnInfoPQ pqSup chatV chatEvent diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index 77ea68274f..c3a28efe68 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -58,7 +58,7 @@ import Simplex.Chat.Controller import Simplex.Chat.Files import Simplex.Chat.Markdown import Simplex.Chat.Messages -import Simplex.Chat.Messages.Batch (BatchMode (..), MsgBatch (..), batchMessages) +import Simplex.Chat.Messages.Batch (BatchMode (..), MsgBatch (..), batchMessages, encodeBinaryBatch, encodeFwdElement) import Simplex.Chat.Messages.CIContent import Simplex.Chat.Messages.CIContent.Events import Simplex.Chat.Operators @@ -1031,7 +1031,7 @@ acceptBusinessJoinRequestAsync -- TODO [short links] get updated business chat group and member? (currently not used) pure (gInfo, clientMember) -acceptRelayJoinRequestAsync :: User -> Int64 -> GroupInfo -> GroupMember -> InvitationId -> VersionRangeChat -> ShortLinkContact -> CM (GroupInfo, GroupMember) +acceptRelayJoinRequestAsync :: User -> Int64 -> GroupInfo -> GroupMember -> InvitationId -> VersionRangeChat -> ShortLinkContact -> MemberKey -> CM (GroupInfo, GroupMember) acceptRelayJoinRequestAsync user uclId @@ -1039,8 +1039,9 @@ acceptRelayJoinRequestAsync _ownerMember@GroupMember {groupMemberId} cReqInvId cReqChatVRange - relayLink = do - let msg = XGrpRelayAcpt relayLink + relayLink + memberKey = do + let msg = XGrpRelayAcpt relayLink memberKey subMode <- chatReadVar subscriptionMode vr <- chatVersionRange let chatV = vr `peerConnChatVersion` cReqChatVRange @@ -1156,13 +1157,13 @@ userProfileInGroup' User {profile = p} allowSimplexLinks incognitoProfile = in redactedMemberProfile allowSimplexLinks p' memberInfo :: GroupInfo -> GroupMember -> MemberInfo -memberInfo g m@GroupMember {memberId, memberRole, memberProfile, activeConn} = +memberInfo g m@GroupMember {memberId, memberRole, memberProfile, memberPubKey, activeConn} = MemberInfo { memberId, memberRole, v = ChatVersionRange . peerChatVRange <$> activeConn, profile = redactedMemberProfile allowSimplexLinks $ fromLocalProfile memberProfile, - memberKey = Nothing -- TODO: get from GroupMember when stored in database + memberKey = MemberKey <$> memberPubKey } where allowSimplexLinks = groupFeatureMemberAllowed SGFSimplexLinks m g @@ -2197,6 +2198,13 @@ sendGroupMemberMessage gInfo@GroupInfo {groupId} m@GroupMember {groupMemberId} c MSAPending -> withStore' $ \db -> createPendingGroupMessage db groupMemberId msgId MSAForwarded -> pure () +-- Send pre-encoded forwarded message preserving original signature +sendFwdMemberMessage :: GroupMember -> GrpMsgForward -> VerifiedMsg 'Json -> CM () +sendFwdMemberMessage member fwd verifiedMsg = + forM_ (readyMemberConn member) $ \(_, conn) -> do + let body = encodeBinaryBatch [encodeFwdElement fwd verifiedMsg] + void $ withAgent $ \a -> sendMessages a [(aConnId conn, PQEncOff, MsgFlags False, VRValue Nothing body)] + -- TODO ensure order - pending messages interleave with user input messages sendPendingGroupMessages :: User -> GroupInfo -> GroupMember -> Connection -> CM () sendPendingGroupMessages user gInfo GroupMember {groupMemberId} conn = do diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 0d17e23d46..3a8fd2f5c6 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -735,12 +735,12 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- [async agent commands] no continuation needed, but command should be asynchronous for stability allowAgentConnectionAsync user conn' confId XOk | otherwise -> messageError "x.grp.acpt: memberId is different from expected" - XGrpRelayAcpt relayLink + XGrpRelayAcpt relayLink memberKey | memberRole' membership == GROwner && isRelay m -> do withStore $ \db -> do relay <- getGroupRelayByGMId db (groupMemberId' m) liftIO $ updateGroupMemberStatus db userId m GSMemAccepted - void $ liftIO $ setRelayLinkAccepted db relay relayLink + void $ liftIO $ setRelayLinkAccepted db relay relayLink memberKey allowAgentConnectionAsync user conn' confId XOk | otherwise -> messageError "x.grp.relay.acpt: only owner can add relay" _ -> messageError "CONF from invited member must have x.grp.acpt" @@ -866,9 +866,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = when (groupFeatureAllowed SGFHistory gInfo'' && not memberIsCustomer) $ sendHistory user gInfo'' m' where sendXGrpLinkMem gInfo'' = do - let incognitoProfile = ExistingIncognito <$> incognitoMembershipProfile gInfo'' + let GroupInfo {membership = membership'} = gInfo'' + incognitoProfile = ExistingIncognito <$> incognitoMembershipProfile gInfo'' profileToSend = userProfileInGroup user gInfo (fromIncognitoProfile <$> incognitoProfile) - void $ sendDirectMemberMessage conn (XGrpLinkMem profileToSend Nothing) groupId -- TODO: send member key + memberKey = MemberKey <$> memberPubKey membership' + void $ sendDirectMemberMessage conn (XGrpLinkMem profileToSend memberKey) groupId _ -> do unless (memberPending m) $ withStore' $ \db -> updateGroupMemberStatus db userId m GSMemConnected notifyMemberConnected gInfo m Nothing @@ -977,7 +979,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = XGrpMemRestrict memId memRestrictions -> fmap ctx <$> xGrpMemRestrict gInfo' m'' memId memRestrictions msg brokerTs XGrpMemCon memId -> Nothing <$ xGrpMemCon gInfo' m'' memId XGrpMemDel memId withMessages -> case encoding @e of - SJson -> fmap ctx <$> xGrpMemDel gInfo' m'' memId withMessages chatMsg msg brokerTs False + SJson -> fmap ctx <$> xGrpMemDel gInfo' m'' memId withMessages verifiedMsg msg brokerTs False SBinary -> pure Nothing XGrpLeave -> fmap ctx <$> xGrpLeave gInfo' m'' msg brokerTs XGrpDel -> Just (DeliveryTaskContext (DJSGroup {jobSpec = DJRelayRemoved}) False) <$ xGrpDel gInfo' m'' msg brokerTs @@ -1107,7 +1109,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = profileToSend = userProfileInGroup user gInfo incognitoProfile g <- asks random (memberPubKey, _memberPrivKey) <- atomically $ C.generateKeyPair g - -- TODO: store memberPrivKey in groups.member_priv_key, memberPubKey in group_members.member_pub_key + -- TODO [member keys] store memberPrivKey in groups.member_priv_key, memberPubKey in group_members.member_pub_key dm <- encodeConnInfo $ XMember profileToSend membershipMemId (MemberKey memberPubKey) subMode <- chatReadVar subscriptionMode void $ joinAgentConnectionAsync user (Just conn) True cReq dm subMode @@ -1433,7 +1435,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- TODO [relays] owner, relays: TBC how to communicate member rejection rules from owner to relays -- TODO [relays] relay: TBC communicate rejection when memberId already exists (currently checked in createJoiningMember) memberJoinRequestViaRelay :: InvitationId -> VersionRangeChat -> Profile -> MemberId -> MemberKey -> CM () - memberJoinRequestViaRelay invId chatVRange p joiningMemberId _joiningMemberKey = do -- TODO: store memberKey in group_members.member_pub_key + memberJoinRequestViaRelay invId chatVRange p joiningMemberId _joiningMemberKey = do -- TODO [member keys] store memberKey in group_members.member_pub_key (_ucl, gLinkInfo_) <- withStore $ \db -> getUserContactLinkById db userId uclId case gLinkInfo_ of Just GroupLinkInfo {groupId, memberRole = gLinkMemRole} -> do @@ -2398,12 +2400,12 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = pure $ memberEventDeliveryScope m xGrpLinkMem :: GroupInfo -> GroupMember -> Connection -> Profile -> Maybe MemberKey -> CM () - xGrpLinkMem gInfo@GroupInfo {membership, businessChat} m@GroupMember {groupMemberId, memberCategory} Connection {viaGroupLink} p' _memberKey = do -- TODO: store memberKey + xGrpLinkMem gInfo@GroupInfo {membership, businessChat} m@GroupMember {groupMemberId, memberCategory} Connection {viaGroupLink} p' memberKey_ = do xGrpLinkMemReceived <- withStore $ \db -> getXGrpLinkMemReceived db groupMemberId if (viaGroupLink || isJust businessChat) && isNothing (memberContactId m) && memberCategory == GCHostMember && not xGrpLinkMemReceived then do m' <- processMemberProfileUpdate gInfo m p' False Nothing - withStore' $ \db -> setXGrpLinkMemReceived db groupMemberId True + withStore' $ \db -> setXGrpLinkMemReceived db groupMemberId True memberKey_ let connectedIncognito = memberIncognito membership probeMatchingMemberContact m' connectedIncognito else messageError "x.grp.link.mem error: invalid group link host profile update" @@ -2800,9 +2802,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = case memberCategory m of GCHostMember -> withStore' (\db -> runExceptT $ getGroupMemberByMemberId db vr user gInfo memId) >>= \case - Right _ -> - unless (useRelays' gInfo) $ - messageError "x.grp.mem.intro ignored: member already exists" + Right existingMember + | useRelays' gInfo -> + void $ withStore $ \db -> updateIntroducedMember db vr user existingMember memInfo + | otherwise -> + messageError "x.grp.mem.intro ignored: member already exists" Left _ | useRelays' gInfo -> void $ withStore $ \db -> createIntroReMember db user gInfo memInfo memRestrictions @@ -2937,8 +2941,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = withStore $ \db -> setMemberVectorRelationConnected db sendingMem refMem MRSubjectConnected withStore $ \db -> setMemberVectorRelationConnected db refMem sendingMem MRReferencedConnected - xGrpMemDel :: GroupInfo -> GroupMember -> MemberId -> Bool -> ChatMessage 'Json -> RcvMessage -> UTCTime -> Bool -> CM (Maybe DeliveryJobScope) - xGrpMemDel gInfo@GroupInfo {membership} m@GroupMember {memberRole = senderRole} memId withMessages chatMsg msg@RcvMessage {msgSigned} brokerTs forwarded = do + xGrpMemDel :: GroupInfo -> GroupMember -> MemberId -> Bool -> VerifiedMsg 'Json -> RcvMessage -> UTCTime -> Bool -> CM (Maybe DeliveryJobScope) + xGrpMemDel gInfo@GroupInfo {membership} m@GroupMember {memberRole = senderRole} memId withMessages verifiedMsg msg@RcvMessage {msgSigned} brokerTs forwarded = do let GroupMember {memberId = membershipMemId} = membership if membershipMemId == memId then checkRole membership $ do @@ -2992,10 +2996,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = | groupFeatureMemberAllowed SGFFullDelete m gInfo' = deleteGroupMemberCIs user gInfo' delMem m msgDir | otherwise = markGroupMemberCIsDeleted user gInfo' delMem m forwardToMember :: GroupMember -> CM () - forwardToMember member = do + forwardToMember member = let fwd = GrpMsgForward {fwdSender = FwdMember (memberId' m) (memberShortenedName m), fwdBrokerTs = brokerTs} - event = XGrpMsgForward fwd chatMsg - sendGroupMemberMessage gInfo member event + in sendFwdMemberMessage member fwd verifiedMsg isUserGrpFwdRelay :: GroupInfo -> Bool isUserGrpFwdRelay gInfo@GroupInfo {membership} @@ -3187,7 +3190,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = XInfo p -> withAuthor XInfo_ $ \author -> void $ xInfoMember gInfo author p msgTs XGrpMemNew memInfo msgScope -> withAuthor XGrpMemNew_ $ \author -> void $ xGrpMemNew gInfo author memInfo msgScope rcvMsg msgTs XGrpMemRole memId memRole -> withAuthor XGrpMemRole_ $ \author -> void $ xGrpMemRole gInfo author memId memRole rcvMsg msgTs - XGrpMemDel memId withMessages -> withAuthor XGrpMemDel_ $ \author -> void $ xGrpMemDel gInfo author memId withMessages chatMsg rcvMsg msgTs True + XGrpMemDel memId withMessages -> withAuthor XGrpMemDel_ $ \author -> void $ xGrpMemDel gInfo author memId withMessages verifiedMsg rcvMsg msgTs True XGrpLeave -> withAuthor XGrpLeave_ $ \author -> void $ xGrpLeave gInfo author rcvMsg msgTs XGrpDel -> withAuthor XGrpDel_ $ \author -> void $ xGrpDel gInfo author rcvMsg msgTs XGrpInfo p' -> withAuthor XGrpInfo_ $ \author -> void $ xGrpInfo gInfo author p' rcvMsg msgTs @@ -3216,9 +3219,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = CBGroup | Just GroupKeys {groupRootKey} <- groupKeys gInfo -> let prefix = smpEncode chatBinding <> smpEncode (groupRootPubKey groupRootKey, memberId) in all (\(MsgSignature KRMember sig) -> C.verify (C.APublicVerifyKey C.SEd25519 pubKey) sig (prefix <> signedBody)) signatures - _ -> True -- can't reconstruct binding → accept (enforcement in Step 5) - | otherwise -> True - Nothing -> not (useRelays' gInfo && requiresSignature (toCMEventTag chatMsgEvent)) + _ -> False + | otherwise -> signatureOptional + Nothing -> signatureOptional + signatureOptional = not (useRelays' gInfo && requiresSignature (toCMEventTag chatMsgEvent)) directMsgReceived :: Contact -> Connection -> MsgMeta -> NonEmpty MsgReceipt -> CM () directMsgReceived ct conn@Connection {connId} msgMeta msgRcpts = do @@ -3564,27 +3568,36 @@ runRelayRequestWorker a Worker {doWork} = do eToView e processRelayRequest :: GroupId -> RelayRequestData -> CM () processRelayRequest groupId rrd = do - gInfo <- withStore $ \db -> getGroupInfo db vr user groupId + (gInfo, groupLink_) <- withStore $ \db -> do + gInfo <- getGroupInfo db vr user groupId + groupLink_ <- liftIO $ runExceptT $ getGroupLink db user gInfo + pure (gInfo, groupLink_) -- Check if relay link already exists (recovery case) - withStore' (\db -> runExceptT $ getGroupLink db user gInfo) >>= \case + case groupLink_ of Right GroupLink {connLinkContact = CCLink _ sLnk_} -> - case sLnk_ of - Just sLnk -> acceptOwnerConnection rrd gInfo sLnk - Nothing -> throwChatError $ CEException "processRelayRequest: relay link doesn't have short link" + case (sLnk_, memberPubKey $ membership gInfo) of + (Just sLnk, Just k) -> acceptOwnerConnection rrd gInfo sLnk (MemberKey k) + (Nothing, _) -> throwChatError $ CEException "processRelayRequest: relay link doesn't have short link" + (_, Nothing) -> throwChatError $ CEException "processRelayRequest: no member key" Left _ -> do - (gInfo', sLnk) <- getLinkDataCreateRelayLink rrd gInfo - acceptOwnerConnection rrd gInfo' sLnk + (gInfo', sLnk, memberKey) <- getLinkDataCreateRelayLink rrd gInfo + acceptOwnerConnection rrd gInfo' sLnk memberKey where - getLinkDataCreateRelayLink :: RelayRequestData -> GroupInfo -> CM (GroupInfo, ShortLinkContact) + getLinkDataCreateRelayLink :: RelayRequestData -> GroupInfo -> CM (GroupInfo, ShortLinkContact, MemberKey) getLinkDataCreateRelayLink RelayRequestData {reqGroupLink} gInfo = do - (_fd, cData) <- getShortLinkConnReq NRMBackground user reqGroupLink + (FixedLinkData {linkEntityId, rootKey}, cData@(ContactLinkData _ UserContactData {owners})) <- getShortLinkConnReq NRMBackground user reqGroupLink liftIO (decodeLinkUserData cData) >>= \case Nothing -> throwChatError $ CEException "getLinkDataCreateRelayLink: no group link data" Just (GroupShortLinkData gp) -> do validateGroupProfile gp - gInfo' <- withStore $ \db -> updateGroupProfile db user gInfo gp + gVar <- asks random + (_, memberPrivKey) <- liftIO $ atomically $ C.generateKeyPair gVar + gInfo' <- withStore $ \db -> do + void $ updateGroupProfile db user gInfo gp + updateRelayGroupKeys db user gInfo linkEntityId rootKey memberPrivKey owners + getGroupInfo db vr user groupId sLnk <- createRelayLink gInfo' - pure (gInfo', sLnk) + pure (gInfo', sLnk, MemberKey $ C.publicKey memberPrivKey) where validateGroupProfile :: GroupProfile -> CM () validateGroupProfile _groupProfile = do @@ -3609,9 +3622,9 @@ runRelayRequestWorker a Worker {doWork} = do gVar <- asks random void $ withFastStore $ \db -> createGroupLink db gVar user gi connId ccLink' groupLinkId subRole subMode pure sLnk - acceptOwnerConnection :: RelayRequestData -> GroupInfo -> ShortLinkContact -> CM () - acceptOwnerConnection RelayRequestData {relayInvId, reqChatVRange} gi relayLink = do + acceptOwnerConnection :: RelayRequestData -> GroupInfo -> ShortLinkContact -> MemberKey -> CM () + acceptOwnerConnection RelayRequestData {relayInvId, reqChatVRange} gi relayLink memberKey = do ownerMember <- withStore $ \db -> getHostMember db vr user groupId - void $ acceptRelayJoinRequestAsync user uclId gi ownerMember relayInvId reqChatVRange relayLink + void $ acceptRelayJoinRequestAsync user uclId gi ownerMember relayInvId reqChatVRange relayLink memberKey -- TODO [relays] relay: group invite accepted event, chat item (?) pure () diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index bb42c19aad..a74dde3f06 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -434,7 +434,7 @@ data ChatMsgEvent (e :: MsgEncoding) where XGrpLinkMem :: Profile -> Maybe MemberKey -> ChatMsgEvent 'Json XGrpLinkAcpt :: GroupAcceptance -> GroupMemberRole -> MemberId -> ChatMsgEvent 'Json XGrpRelayInv :: GroupRelayInvitation -> ChatMsgEvent 'Json - XGrpRelayAcpt :: ShortLinkContact -> ChatMsgEvent 'Json + XGrpRelayAcpt :: ShortLinkContact -> MemberKey -> ChatMsgEvent 'Json XGrpMemNew :: MemberInfo -> Maybe MsgScope -> ChatMsgEvent 'Json XGrpMemIntro :: MemberInfo -> Maybe MemberRestrictions -> ChatMsgEvent 'Json XGrpMemInv :: MemberId -> IntroInvitation -> ChatMsgEvent 'Json @@ -1128,7 +1128,7 @@ toCMEventTag msg = case msg of XGrpLinkMem _ _ -> XGrpLinkMem_ XGrpLinkAcpt {} -> XGrpLinkAcpt_ XGrpRelayInv _ -> XGrpRelayInv_ - XGrpRelayAcpt _ -> XGrpRelayAcpt_ + XGrpRelayAcpt _ _ -> XGrpRelayAcpt_ XGrpMemNew {} -> XGrpMemNew_ XGrpMemIntro _ _ -> XGrpMemIntro_ XGrpMemInv _ _ -> XGrpMemInv_ @@ -1260,7 +1260,7 @@ appJsonToCM AppMessageJson {v, msgId, event, params} = do XGrpLinkMem_ -> XGrpLinkMem <$> p "profile" <*> opt "memberKey" XGrpLinkAcpt_ -> XGrpLinkAcpt <$> p "acceptance" <*> p "role" <*> p "memberId" XGrpRelayInv_ -> XGrpRelayInv <$> p "groupRelayInvitation" - XGrpRelayAcpt_ -> XGrpRelayAcpt <$> p "relayLink" + XGrpRelayAcpt_ -> XGrpRelayAcpt <$> p "relayLink" <*> p "memberKey" XGrpMemNew_ -> XGrpMemNew <$> p "memberInfo" <*> opt "scope" XGrpMemIntro_ -> XGrpMemIntro <$> p "memberInfo" <*> opt "memberRestrictions" XGrpMemInv_ -> XGrpMemInv <$> p "memberId" <*> p "memberIntro" @@ -1327,7 +1327,7 @@ chatToAppMessage chatMsg@ChatMessage {chatVRange, msgId, chatMsgEvent} = case en XGrpLinkMem profile memberKey -> o $ ("memberKey" .=? memberKey) ["profile" .= profile] XGrpLinkAcpt acceptance role memberId -> o ["acceptance" .= acceptance, "role" .= role, "memberId" .= memberId] XGrpRelayInv groupRelayInv -> o ["groupRelayInvitation" .= groupRelayInv] - XGrpRelayAcpt relayLink -> o ["relayLink" .= relayLink] + XGrpRelayAcpt relayLink memberKey -> o ["relayLink" .= relayLink, "memberKey" .= memberKey] XGrpMemNew memInfo scope -> o $ ("scope" .=? scope) ["memberInfo" .= memInfo] XGrpMemIntro memInfo memRestrictions -> o $ ("memberRestrictions" .=? memRestrictions) ["memberInfo" .= memInfo] XGrpMemInv memId memIntro -> o ["memberId" .= memId, "memberIntro" .= memIntro] diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 3ad4e3c71c..323b23449e 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -101,6 +101,7 @@ module Simplex.Chat.Store.Groups createMemberConnection, createMemberConnectionAsync, updateGroupMemberKeys, + updateRelayGroupKeys, updateGroupMemberStatus, updateGroupMemberStatusById, updateGroupMemberAccepted, @@ -149,6 +150,8 @@ module Simplex.Chat.Store.Groups getXGrpLinkMemReceived, setXGrpLinkMemReceived, createNewUnknownGroupMember, + createLinkOwnerMember, + updateIntroducedMember, updateUnknownMemberAnnounced, updateUserMemberProfileSentAt, setGroupCustomData, @@ -190,7 +193,7 @@ import Simplex.Chat.Types.MemberRelations (IntroductionDirection (..), MemberRel import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared import Simplex.Chat.Types.UITheme -import Simplex.Messaging.Agent.Protocol (ConnId, CreatedConnLink (..), InvitationId, UserId) +import Simplex.Messaging.Agent.Protocol (ConnId, CreatedConnLink (..), InvitationId, OwnerAuth (..), UserId) import Simplex.Messaging.Agent.Store.AgentStore (firstRow, fromOnlyBI, maybeFirstRow) import Simplex.Messaging.Agent.Store.DB (Binary (..), BoolInt (..)) import Simplex.Messaging.Agent.Store.Entity (DBEntityId) @@ -1396,8 +1399,8 @@ updateRelayStatus_ db relayId relayStatus = do currentTs <- getCurrentTime DB.execute db "UPDATE group_relays SET relay_status = ?, updated_at = ? WHERE group_relay_id = ?" (relayStatus, currentTs, relayId) -setRelayLinkAccepted :: DB.Connection -> GroupRelay -> ShortLinkContact -> IO GroupRelay -setRelayLinkAccepted db relay@GroupRelay {groupRelayId, groupMemberId} relayLink = do +setRelayLinkAccepted :: DB.Connection -> GroupRelay -> ShortLinkContact -> MemberKey -> IO GroupRelay +setRelayLinkAccepted db relay@GroupRelay {groupRelayId, groupMemberId} relayLink (MemberKey k) = do currentTs <- getCurrentTime DB.execute db @@ -1411,10 +1414,10 @@ setRelayLinkAccepted db relay@GroupRelay {groupRelayId, groupMemberId} relayLink db [sql| UPDATE group_members - SET relay_link = ?, updated_at = ? + SET relay_link = ?, member_pub_key = ?, updated_at = ? WHERE group_member_id = ? |] - (relayLink, currentTs, groupMemberId) + (relayLink, k, currentTs, groupMemberId) pure relay {relayStatus = RSAccepted, relayLink = Just relayLink} setGroupInProgressDone :: DB.Connection -> GroupInfo -> IO () @@ -1707,6 +1710,28 @@ updateGroupMemberKeys db groupId sharedGroupId rootPubKey memberPrivKey membersh "UPDATE group_members SET member_pub_key = ?, updated_at = ? WHERE group_member_id = ?" (C.publicKey memberPrivKey, currentTs, membershipGMId) +updateRelayGroupKeys :: DB.Connection -> User -> GroupInfo -> Maybe ByteString -> C.PublicKeyEd25519 -> C.PrivateKeyEd25519 -> [OwnerAuth] -> ExceptT StoreError IO () +updateRelayGroupKeys db user gInfo linkEntityId rootPubKey memberPrivKey owners = do + currentTs <- liftIO getCurrentTime + let membershipGMId = groupMemberId' $ membership gInfo + liftIO $ do + DB.execute + db + "UPDATE groups SET shared_group_id = ?, root_pub_key = ?, member_priv_key = ?, updated_at = ? WHERE group_id = ?" + (Binary <$> linkEntityId, rootPubKey, memberPrivKey, currentTs, groupId' gInfo) + DB.execute + db + "UPDATE group_members SET member_pub_key = ?, updated_at = ? WHERE group_member_id = ?" + (C.publicKey memberPrivKey, currentTs, membershipGMId) + -- TODO [relays] relay: if not found, create owner record (multi-owner) + forM_ owners $ \OwnerAuth {ownerId, ownerKey} -> do + ownerGMId <- getGroupMemberIdViaMemberId db user gInfo (MemberId ownerId) + liftIO $ + DB.execute + db + "UPDATE group_members SET member_pub_key = ?, updated_at = ? WHERE group_member_id = ?" + (ownerKey, currentTs, ownerGMId) + updateGroupMemberStatus :: DB.Connection -> UserId -> GroupMember -> GroupMemberStatus -> IO () updateGroupMemberStatus db userId GroupMember {groupMemberId} = updateGroupMemberStatusById db userId groupMemberId @@ -1838,7 +1863,7 @@ createNewMember_ User {userId, userContactId} GroupInfo {groupId} NewGroupMember - { memInfo = MemberInfo memberId memberRole memChatVRange memberProfile _memKey, + { memInfo = MemberInfo memberId memberRole memChatVRange memberProfile memKey, memCategory = memberCategory, memStatus = memberStatus, memRestriction, @@ -1852,6 +1877,7 @@ createNewMember_ let invitedById = fromInvitedBy userContactId invitedBy activeConn = Nothing memberChatVRange@(VersionRange minV maxV) = maybe chatInitialVRange fromChatVRange memChatVRange + memberPubKey = (\(MemberKey k) -> k) <$> memKey indexInGroup <- getUpdateNextIndexInGroup_ db groupId liftIO $ DB.execute @@ -1860,13 +1886,13 @@ createNewMember_ INSERT INTO group_members (group_id, index_in_group, member_id, member_role, member_category, member_status, member_relations_vector, member_restriction, invited_by, invited_by_group_member_id, - user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at, + user_id, local_display_name, contact_id, contact_profile_id, member_pub_key, created_at, updated_at, peer_chat_min_version, peer_chat_max_version) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] ( (groupId, indexInGroup, memberId, memberRole, memberCategory, memberStatus, Binary B.empty) :. (memRestriction, invitedById, memInvitedByGroupMemberId) - :. (userId, localDisplayName, memberContactId, memberContactProfileId, createdAt, createdAt) + :. (userId, localDisplayName, memberContactId, memberContactProfileId, memberPubKey, createdAt, createdAt) :. (minV, maxV) ) groupMemberId <- liftIO $ insertedRowId db @@ -1892,8 +1918,7 @@ createNewMember_ createdAt, updatedAt = createdAt, supportChat = Nothing, - -- TODO [member keys] is it used with relay/public groups? - memberPubKey = Nothing, + memberPubKey, relayLink = Nothing } @@ -2767,13 +2792,14 @@ getXGrpLinkMemReceived db mId = ExceptT . firstRow fromOnlyBI (SEGroupMemberNotFound mId) $ DB.query db "SELECT xgrplinkmem_received FROM group_members WHERE group_member_id = ?" (Only mId) -setXGrpLinkMemReceived :: DB.Connection -> GroupMemberId -> Bool -> IO () -setXGrpLinkMemReceived db mId xGrpLinkMemReceived = do +setXGrpLinkMemReceived :: DB.Connection -> GroupMemberId -> Bool -> Maybe MemberKey -> IO () +setXGrpLinkMemReceived db mId xGrpLinkMemReceived memberKey_ = do currentTs <- getCurrentTime + let k = (\(MemberKey k') -> k') <$> memberKey_ DB.execute db - "UPDATE group_members SET xgrplinkmem_received = ?, updated_at = ? WHERE group_member_id = ?" - (BI xGrpLinkMemReceived, currentTs, mId) + "UPDATE group_members SET xgrplinkmem_received = ?, member_pub_key = ?, updated_at = ? WHERE group_member_id = ?" + (BI xGrpLinkMemReceived, k, currentTs, mId) createNewUnknownGroupMember :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> MemberId -> Text -> GroupMemberRole -> ExceptT StoreError IO GroupMember createNewUnknownGroupMember db vr user@User {userId, userContactId} GroupInfo {groupId} memberId memberName unknownMemberRole = do @@ -2800,6 +2826,52 @@ createNewUnknownGroupMember db vr user@User {userId, userContactId} GroupInfo {g where VersionRange minV maxV = vr +createLinkOwnerMember :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> MemberId -> C.PublicKeyEd25519 -> ExceptT StoreError IO GroupMember +createLinkOwnerMember db vr user@User {userId, userContactId} GroupInfo {groupId} memberId ownerKey = do + currentTs <- liftIO getCurrentTime + let memberProfile = profileFromName $ nameFromMemberId memberId + (localDisplayName, profileId) <- createNewMemberProfile_ db user memberProfile currentTs + indexInGroup <- getUpdateNextIndexInGroup_ db groupId + liftIO $ + DB.execute + db + [sql| + INSERT INTO group_members + ( group_id, index_in_group, member_id, member_role, member_category, member_status, member_relations_vector, invited_by, + user_id, local_display_name, contact_id, contact_profile_id, member_pub_key, created_at, updated_at, + peer_chat_min_version, peer_chat_max_version) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + |] + ( (groupId, indexInGroup, memberId, GROwner, GCPreMember, GSMemUnknown, Binary B.empty, fromInvitedBy userContactId IBUnknown) + :. (userId, localDisplayName, Nothing :: (Maybe Int64), profileId, ownerKey, currentTs, currentTs) + :. (minV, maxV) + ) + groupMemberId <- liftIO $ insertedRowId db + getGroupMemberById db vr user groupMemberId + where + VersionRange minV maxV = vr + +updateIntroducedMember :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> MemberInfo -> ExceptT StoreError IO GroupMember +updateIntroducedMember db vr user@User {userId} member@GroupMember {groupMemberId, memberChatVRange} MemberInfo {memberRole, v, profile} = do + _ <- updateMemberProfile db user member profile + currentTs <- liftIO getCurrentTime + liftIO $ + DB.execute + db + [sql| + UPDATE group_members + SET member_role = ?, + member_status = ?, + peer_chat_min_version = ?, + peer_chat_max_version = ?, + updated_at = ? + WHERE user_id = ? AND group_member_id = ? + |] + (memberRole, GSMemIntroduced, minV, maxV, currentTs, userId, groupMemberId) + getGroupMemberById db vr user groupMemberId + where + VersionRange minV maxV = maybe memberChatVRange fromChatVRange v + updateUnknownMemberAnnounced :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> GroupMember -> MemberInfo -> GroupMemberStatus -> ExceptT StoreError IO GroupMember updateUnknownMemberAnnounced db vr user@User {userId} invitingMember unknownMember@GroupMember {groupMemberId, memberChatVRange} MemberInfo {memberRole, v, profile} status = do _ <- updateMemberProfile db user unknownMember profile diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt index 1b881bd446..b722158ef9 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt @@ -273,6 +273,16 @@ Query: Plan: SEARCH connections USING PRIMARY KEY (conn_id=?) +Query: + SELECT user_id FROM users u + WHERE u.deleted = ? + AND NOT EXISTS (SELECT c.conn_id FROM connections c WHERE c.user_id = u.user_id) + +Plan: +SCAN u +CORRELATED SCALAR SUBQUERY 1 +SEARCH c USING COVERING INDEX idx_connections_user (user_id=?) + Query: SELECT user_id FROM users u WHERE u.user_id = ? @@ -525,6 +535,54 @@ Query: Plan: SEARCH conn_confirmations USING COVERING INDEX idx_conn_confirmations_conn_id (conn_id=?) +Query: + DELETE FROM encrypted_rcv_message_hashes + WHERE encrypted_rcv_message_hash_id IN ( + SELECT encrypted_rcv_message_hash_id + FROM encrypted_rcv_message_hashes + WHERE created_at < ? + ORDER BY created_at ASC + LIMIT ? + ) + +Plan: +SEARCH encrypted_rcv_message_hashes USING INTEGER PRIMARY KEY (rowid=?) +LIST SUBQUERY 1 +SEARCH encrypted_rcv_message_hashes USING COVERING INDEX idx_encrypted_rcv_message_hashes_created_at (created_at ("/_get chat #1 count=1", chat, [(1, "removed eve (signed)")]) bob #$> ("/_get chat #1 count=1", chat, [(0, "removed eve (signed)")])