mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-23 19:23:36 +00:00
core: signed XMember in public group (#7036)
This commit is contained in:
1 parent
aed17866cb
commit
0773ccd057
7 files changed
+269
-110
No files matched your search
@@ -3594,13 +3594,18 @@ processChatCommand vr nm = \case
|
||||
where
|
||||
cReqHash1 = contactCReqHash $ CRContactUri crData {crScheme = SSSimplex}
|
||||
cReqHash2 = contactCReqHash $ CRContactUri crData {crScheme = simplexChat}
|
||||
-- relay-group joins (only via connectToRelay) carry the target relay member in preparedEntity_;
|
||||
-- its memberId binds the join signature so a sibling relay can't replay it
|
||||
relayMemberId_ = case preparedEntity_ of
|
||||
Just (PCEGroup gInfo m) | useRelays' gInfo -> Just (memberId' m)
|
||||
_ -> Nothing
|
||||
joinPreparedConn' xContactId_ conn@Connection {customUserProfileId} gInfo_ = do
|
||||
when (incognito /= isJust customUserProfileId) $ throwCmdError "incognito mode is different from prepared connection"
|
||||
-- TODO [relays] member: refactor joinContact and up avoiding parallel ifs, xContactId is not used
|
||||
xContactId <- mkXContactId xContactId_
|
||||
localIncognitoProfile <- forM customUserProfileId $ \pId -> withFastStore $ \db -> getProfileById db userId pId
|
||||
let incognitoProfile = fromLocalProfile <$> localIncognitoProfile
|
||||
conn' <- joinContact user conn cReq incognitoProfile xContactId welcomeSharedMsgId msg_ gInfo_ PQSupportOn
|
||||
conn' <- joinContact user conn cReq incognitoProfile xContactId welcomeSharedMsgId msg_ gInfo_ relayMemberId_ PQSupportOn
|
||||
pure $ CVRSentInvitation conn' incognitoProfile
|
||||
connect' groupLinkId xContactId_ gInfo_ = do
|
||||
let inGroup = isJust groupLinkId
|
||||
@@ -3615,7 +3620,7 @@ processChatCommand vr nm = \case
|
||||
subMode <- chatReadVar subscriptionMode
|
||||
let sLnk' = serverShortLink <$> sLnk
|
||||
conn <- withFastStore' $ \db -> createConnReqConnection db userId connId preparedEntity_ cReq cReqHash1 sLnk' xContactId incognitoProfile_ groupLinkId subMode chatV pqSup
|
||||
conn' <- joinContact user conn cReq incognitoProfile xContactId welcomeSharedMsgId msg_ gInfo_ pqSup
|
||||
conn' <- joinContact user conn cReq incognitoProfile xContactId welcomeSharedMsgId msg_ gInfo_ relayMemberId_ pqSup
|
||||
pure $ CVRSentInvitation conn' incognitoProfile
|
||||
connectContactViaAddress :: User -> IncognitoEnabled -> Contact -> CreatedLinkContact -> CM ChatResponse
|
||||
connectContactViaAddress user@User {userId} incognito ct@Contact {contactId, activeConn} (CCLink cReq shortLink) =
|
||||
@@ -3630,7 +3635,7 @@ processChatCommand vr nm = \case
|
||||
subMode <- chatReadVar subscriptionMode
|
||||
let cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq
|
||||
conn <- withFastStore' $ \db -> createConnReqConnection db userId connId (Just $ PCEContact ct) cReq cReqHash shortLink newXContactId (NewIncognito <$> incognitoProfile) Nothing subMode chatV pqSup
|
||||
void $ joinContact user conn cReq incognitoProfile newXContactId Nothing Nothing Nothing pqSup
|
||||
void $ joinContact user conn cReq incognitoProfile newXContactId Nothing Nothing Nothing Nothing pqSup
|
||||
ct' <- withStore $ \db -> getContact db vr user contactId
|
||||
pure $ CRSentInvitationToContact user ct' incognitoProfile
|
||||
Just conn@Connection {connStatus, xContactId = xContactId_, customUserProfileId} -> case connStatus of
|
||||
@@ -3639,7 +3644,7 @@ processChatCommand vr nm = \case
|
||||
xContactId <- mkXContactId xContactId_
|
||||
localIncognitoProfile <- forM customUserProfileId $ \pId -> withFastStore $ \db -> getProfileById db userId pId
|
||||
let incognitoProfile = fromLocalProfile <$> localIncognitoProfile
|
||||
void $ joinContact user conn cReq incognitoProfile xContactId Nothing Nothing Nothing PQSupportOn
|
||||
void $ joinContact user conn cReq incognitoProfile xContactId Nothing Nothing Nothing Nothing PQSupportOn
|
||||
ct' <- withStore $ \db -> getContact db vr user contactId
|
||||
pure $ CRSentInvitationToContact user ct' incognitoProfile
|
||||
_ -> throwCmdError "contact already has connection"
|
||||
@@ -3651,13 +3656,14 @@ processChatCommand vr nm = \case
|
||||
r <- tryAllErrors $ do
|
||||
(fd@FixedLinkData {rootKey = relayKey, linkEntityId}, cData) <- getShortLinkConnReq nm user relayLink
|
||||
relayLinkData_ <- liftIO $ decodeLinkUserData cData
|
||||
case (relayLinkData_, linkEntityId) of
|
||||
(Just RelayShortLinkData {relayProfile = p}, Just entityId) ->
|
||||
relayMemberId <- case (relayLinkData_, linkEntityId) of
|
||||
(Just RelayShortLinkData {relayProfile = p}, Just entityId) -> do
|
||||
withFastStore $ \db -> updateRelayMemberData db user relayMember (MemberId entityId) (MemberKey relayKey) p
|
||||
pure $ MemberId entityId
|
||||
_ -> throwChatError $ CEException "relay link: no relay link data or entity id"
|
||||
let cReq = linkConnReq fd
|
||||
relayLinkToConnect = CCLink cReq (Just relayLink)
|
||||
void $ connectViaContact user (Just $ PCEGroup gInfo relayMember) (incognitoMembership gInfo) relayLinkToConnect Nothing Nothing
|
||||
void $ connectViaContact user (Just $ PCEGroup gInfo (relayMember {memberId = relayMemberId})) (incognitoMembership gInfo) relayLinkToConnect Nothing Nothing
|
||||
relayMember' <- withFastStore $ \db -> getGroupMember db vr user (groupId' gInfo) (groupMemberId' relayMember)
|
||||
pure (relayLink, relayMember', r)
|
||||
syncSubscriberRelays :: User -> GroupInfo -> [ShortLinkContact] -> CM ()
|
||||
@@ -3693,23 +3699,19 @@ processChatCommand vr nm = \case
|
||||
pure (connId, chatV)
|
||||
mkXContactId :: Maybe XContactId -> CM XContactId
|
||||
mkXContactId = maybe (XContactId <$> drgRandomBytes 16) pure
|
||||
joinContact :: User -> Connection -> ConnReqContact -> Maybe Profile -> XContactId -> Maybe SharedMsgId -> Maybe (SharedMsgId, MsgContent) -> Maybe (Maybe GroupInfo) -> PQSupport -> CM Connection
|
||||
joinContact user conn@Connection {connChatVersion = chatV} cReq incognitoProfile xContactId welcomeSharedMsgId msg_ gInfo_ pqSup = do
|
||||
joinContact :: User -> Connection -> ConnReqContact -> Maybe Profile -> XContactId -> Maybe SharedMsgId -> Maybe (SharedMsgId, MsgContent) -> Maybe (Maybe GroupInfo) -> Maybe MemberId -> PQSupport -> CM Connection
|
||||
joinContact user conn@Connection {connChatVersion = chatV} cReq incognitoProfile xContactId welcomeSharedMsgId msg_ gInfo_ relayMemberId_ pqSup = do
|
||||
-- gInfo_ is Maybe (Maybe GroupInfo), where Just Nothing means "some unknown group", e.g. when joining via link without profile
|
||||
let profileToSend = case gInfo_ of
|
||||
Just gInfo_' ->
|
||||
let allowSimplexLinks = maybe True (groupFeatureUserAllowed SGFSimplexLinks) gInfo_'
|
||||
in userProfileInGroup' user allowSimplexLinks incognitoProfile
|
||||
Nothing -> userProfileDirect user incognitoProfile Nothing True
|
||||
chatEvent <- case gInfo_ of
|
||||
Just (Just gInfo) | useRelays' gInfo -> do
|
||||
let GroupInfo {membership = GroupMember {memberId}} = gInfo
|
||||
memberPubKey <- case groupKeys gInfo of
|
||||
Just GroupKeys {memberPrivKey} -> pure $ C.publicKey memberPrivKey
|
||||
Nothing -> throwChatError $ CEInternalError "no group keys for channel membership"
|
||||
pure $ XMember profileToSend memberId (MemberKey memberPubKey)
|
||||
_ -> pure $ XContact profileToSend (Just xContactId) welcomeSharedMsgId msg_
|
||||
dm <- encodeConnInfoPQ pqSup chatV chatEvent
|
||||
dm <- case gInfo_ of
|
||||
Just (Just gInfo) | useRelays' gInfo -> case relayMemberId_ of
|
||||
Just relayMemberId -> encodeXMemberConnInfo gInfo relayMemberId profileToSend
|
||||
Nothing -> throwChatError $ CEInternalError "relay group join without target relay memberId"
|
||||
_ -> encodeConnInfoPQ pqSup chatV $ XContact profileToSend (Just xContactId) welcomeSharedMsgId msg_
|
||||
subMode <- chatReadVar subscriptionMode
|
||||
void $ withAgent $ \a -> joinConnection a nm (aUserId user) (aConnId conn) True cReq dm pqSup subMode
|
||||
withFastStore' $ \db -> updateConnectionStatusFromTo db conn ConnPrepared ConnJoined
|
||||
|
||||
@@ -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, encodeBinaryBatch, encodeFwdElement)
|
||||
import Simplex.Chat.Messages.Batch (BatchMode (..), MsgBatch (..), batchMessages, encodeBatchElement, encodeBinaryBatch, encodeFwdElement)
|
||||
import Simplex.Chat.Messages.CIContent
|
||||
import Simplex.Chat.Messages.CIContent.Events
|
||||
import Simplex.Chat.Operators
|
||||
@@ -927,9 +927,9 @@ acceptContactRequestAsync
|
||||
liftIO $ setCommandConnId db user cmdId connId
|
||||
getContact db vr user contactId
|
||||
|
||||
acceptGroupJoinRequestAsync :: User -> Int64 -> GroupInfo -> InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> Maybe MemberId -> Maybe SharedMsgId -> GroupAcceptance -> GroupMemberRole -> Maybe IncognitoProfile -> Maybe MemberKey -> CM GroupMember
|
||||
acceptGroupJoinRequestAsync :: User -> Int64 -> GroupInfo -> InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> Maybe MemberId -> Maybe SharedMsgId -> GroupAcceptance -> GroupMemberRole -> Maybe IncognitoProfile -> Maybe MemberKey -> Maybe GroupMember -> CM GroupMember
|
||||
acceptGroupJoinRequestAsync
|
||||
user
|
||||
user@User {userId}
|
||||
uclId
|
||||
gInfo@GroupInfo {groupProfile, membership, businessChat}
|
||||
cReqInvId
|
||||
@@ -941,11 +941,18 @@ acceptGroupJoinRequestAsync
|
||||
gAccepted
|
||||
gLinkMemRole
|
||||
incognitoProfile
|
||||
memberKey_ = do
|
||||
memberKey_
|
||||
existingMem_ = do
|
||||
gVar <- asks random
|
||||
let initialStatus = acceptanceToStatus (memberAdmission groupProfile) gAccepted
|
||||
(groupMemberId, memberId) <- withStore $ \db ->
|
||||
createJoiningMember db gVar user gInfo cReqChatVRange cReqProfile cReqXContactId_ cReqMemberId_ welcomeMsgId_ gLinkMemRole initialStatus memberKey_
|
||||
-- a roster-established privileged member attaches a connection to its existing record (keeping
|
||||
-- owner-authoritative role + key); everyone else is created fresh with the group-link role
|
||||
(groupMemberId, memberId) <- case existingMem_ of
|
||||
Just m -> do
|
||||
withStore' $ \db -> updateGroupMemberStatus db userId m initialStatus
|
||||
pure (groupMemberId' m, memberId' m)
|
||||
Nothing -> withStore $ \db ->
|
||||
createJoiningMember db gVar user gInfo cReqChatVRange cReqProfile cReqXContactId_ cReqMemberId_ welcomeMsgId_ gLinkMemRole initialStatus memberKey_
|
||||
let currentMemCount = fromIntegral $ currentMembers $ groupSummary gInfo
|
||||
let Profile {displayName} = userProfileInGroup user gInfo (fromIncognitoProfile <$> incognitoProfile)
|
||||
GroupMember {memberRole = userRole, memberId = userMemberId} = membership
|
||||
@@ -1160,12 +1167,12 @@ memberIntroEvt gInfo reMember =
|
||||
mRestrictions = memberRestrictions reMember
|
||||
in XGrpMemIntro mInfo mRestrictions
|
||||
|
||||
-- Forward the cached owner-signed roster verbatim, attributed to the owner who
|
||||
-- Forward the saved owner-signed roster verbatim, attributed to the owner who
|
||||
-- sent it, so the recipient verifies the owner signature.
|
||||
forwardCachedRoster :: User -> GroupInfo -> GroupMember -> CM ()
|
||||
forwardCachedRoster user gInfo subscriber = do
|
||||
forwardGroupRoster :: User -> GroupInfo -> GroupMember -> CM ()
|
||||
forwardGroupRoster user gInfo subscriber = do
|
||||
vr <- chatVersionRange
|
||||
withStore' (\db -> getCachedGroupRoster db gInfo) >>= \case
|
||||
withStore' (\db -> getGroupRoster db gInfo) >>= \case
|
||||
Nothing -> pure ()
|
||||
Just (ownerGMId, brokerTs, sm@SignedMsg {signedBody}) ->
|
||||
forM_ (eitherToMaybe (J.eitherDecodeStrict' signedBody) :: Maybe (ChatMessage 'Json)) $ \chatMsg ->
|
||||
@@ -1187,9 +1194,9 @@ introduceInChannel vr user gInfo subscriber@GroupMember {activeConn = Just conn,
|
||||
void $ sendGroupMessage' user gInfo modMs $ XGrpMemNew (memberInfo gInfo subscriber) Nothing
|
||||
withStore' $ \db ->
|
||||
setMemberVectorNewRelations db subscriber [(indexInGroup m, (IDSubjectIntroduced, MRIntroduced)) | m <- modMs]
|
||||
-- owner intros first so the joiner has the owner profile loaded before applying the cached roster (signed by the owner)
|
||||
-- owner intros first so the joiner has the owner profile loaded before applying the saved roster (signed by the owner)
|
||||
sendIntros owners
|
||||
forwardCachedRoster user gInfo subscriber
|
||||
forwardGroupRoster user gInfo subscriber
|
||||
sendIntros rosterMems
|
||||
withStore' $ \db ->
|
||||
setMembersVectorsNewRelation db modMs subscriberIdx IDSubjectIntroduced MRIntroduced
|
||||
@@ -1659,13 +1666,16 @@ sendFileInline_ FileTransferMeta {filePath, chunkSize} sharedMsgId sendMsg =
|
||||
chSize = fromIntegral chunkSize
|
||||
|
||||
parseChatMessage :: Connection -> ByteString -> CM (ChatMessage 'Json)
|
||||
parseChatMessage conn s = do
|
||||
parseChatMessage conn s = snd <$> parseChatMessage' conn s
|
||||
{-# INLINE parseChatMessage #-}
|
||||
|
||||
parseChatMessage' :: Connection -> ByteString -> CM (Maybe SignedMsg, ChatMessage 'Json)
|
||||
parseChatMessage' conn s =
|
||||
case parseChatMessages s of
|
||||
[msg] -> liftEither . first (ChatError . errType) $ (\(APMsg _ (ParsedMsg _ _ m)) -> checkEncoding m) =<< msg
|
||||
[msg] -> liftEither . first (ChatError . errType) $ (\(APMsg _ (ParsedMsg _ sm m)) -> (sm,) <$> checkEncoding m) =<< msg
|
||||
_ -> throwChatError $ CEException "parseChatMessage: single message is expected"
|
||||
where
|
||||
errType = CEInvalidChatMessage conn Nothing (safeDecodeUtf8 s)
|
||||
{-# INLINE parseChatMessage #-}
|
||||
|
||||
getChatScopeInfo :: VersionRangeChat -> User -> GroupChatScope -> CM GroupChatScopeInfo
|
||||
getChatScopeInfo vr user = \case
|
||||
@@ -2067,6 +2077,26 @@ encodeConnInfoPQ pqSup v chatMsgEvent = do
|
||||
_ -> pure connInfo
|
||||
ECMLarge -> throwChatError $ CEException "large info"
|
||||
|
||||
-- conn-info wrapped as a signed element, so the receiver can verify the signature over the body
|
||||
encodeSignedConnInfo :: MsgEncodingI e => MsgSigning -> ChatMsgEvent e -> CM ByteString
|
||||
encodeSignedConnInfo signing chatMsgEvent = do
|
||||
vr <- chatVersionRange
|
||||
let info = ChatMessage {chatVRange = vr, msgId = Nothing, chatMsgEvent}
|
||||
case encodeChatMessage maxEncodedInfoLength info of
|
||||
ECMEncoded body -> pure $ encodeBatchElement (Just $ signChatMsgBody signing body) body
|
||||
ECMLarge -> throwChatError $ CEException "large signed info"
|
||||
|
||||
-- signed XMember for a relay-group join: proves the joiner holds the member key it asserts, and carries
|
||||
-- viaRelay = the target relay's memberId inside the signed body so a sibling relay can't accept a replay
|
||||
encodeXMemberConnInfo :: GroupInfo -> MemberId -> Profile -> CM ByteString
|
||||
encodeXMemberConnInfo GroupInfo {membership = GroupMember {memberId}, groupKeys} relayMemberId profileToSend =
|
||||
case groupKeys of
|
||||
Just GroupKeys {publicGroupId, memberPrivKey} ->
|
||||
let xMemberEvt = XMember profileToSend memberId (MemberKey $ C.publicKey memberPrivKey) (Just relayMemberId)
|
||||
signing = MsgSigning CBGroup (smpEncode (publicGroupId, memberId)) KRMember memberPrivKey
|
||||
in encodeSignedConnInfo signing xMemberEvt
|
||||
Nothing -> throwChatError $ CEInternalError "no group keys for channel membership"
|
||||
|
||||
deliverMessage :: Connection -> CMEventTag e -> MsgBody -> MessageId -> CM (Int64, PQEncryption)
|
||||
deliverMessage conn cmEventTag msgBody msgId = do
|
||||
let msgFlags = MsgFlags {notification = hasNotification cmEventTag}
|
||||
|
||||
@@ -105,6 +105,13 @@ import UnliftIO.STM
|
||||
smallGroupsRcptsMemLimit :: Int
|
||||
smallGroupsRcptsMemLimit = 20
|
||||
|
||||
-- Verifies member signatures over CBGroup <> (publicGroupId, memberId) <> signedBody under the given key.
|
||||
-- signatures is NonEmpty so the verification can't be vacuously true.
|
||||
verifyGroupSig :: C.PublicKeyEd25519 -> B64UrlByteString -> MemberId -> NonEmpty MsgSignature -> ByteString -> Bool
|
||||
verifyGroupSig key publicGroupId memberId signatures signedBody =
|
||||
let prefix = smpEncode CBGroup <> smpEncode (publicGroupId, memberId)
|
||||
in all (\(MsgSignature KRMember sig) -> C.verify (C.APublicVerifyKey C.SEd25519 key) sig (prefix <> signedBody)) signatures
|
||||
|
||||
processAgentMessage :: ACorrId -> ConnId -> AEvent 'AEConn -> CM ()
|
||||
processAgentMessage _ _ (DEL_RCVQS delQs) =
|
||||
toView $ CEvtAgentRcvQueuesDeleted $ L.map rcvQ delQs
|
||||
@@ -893,8 +900,14 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
GCInviteeMember
|
||||
| isRelay m -> do
|
||||
withStore' $ \db -> updateGroupMemberStatus db userId m GSMemConnected
|
||||
gLink <- withStore $ \db -> getGroupLink db user gInfo
|
||||
setGroupLinkDataAsync user gInfo gLink
|
||||
-- always send the relay a roster (materializing version 0 for old channels with NULL roster_version);
|
||||
-- the relay stays RSInvited (unpublishable) until it acks, so no joiner can impersonate a privileged member
|
||||
gInfo' <- case rosterVersion gInfo of
|
||||
Just _ -> pure gInfo
|
||||
Nothing -> do
|
||||
withStore' $ \db -> setGroupRosterVersion db gInfo (VersionRoster 0)
|
||||
pure gInfo {rosterVersion = Just (VersionRoster 0)}
|
||||
sendGroupRosterToRelay user gInfo' m
|
||||
| otherwise -> do
|
||||
(gInfo', mStatus) <-
|
||||
if not (memberPending m)
|
||||
@@ -1066,6 +1079,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
XGrpInfo p' -> fmap ctx <$> xGrpInfo gInfo' m'' p' msg brokerTs
|
||||
XGrpPrefs ps' -> fmap ctx <$> xGrpPrefs gInfo' m'' ps' msg
|
||||
XGrpRoster gr -> fmap ctx <$> xGrpRoster gInfo' m'' gr verifiedMsg brokerTs
|
||||
XGrpRosterAck ackVer ackErr -> Nothing <$ xGrpRosterAck gInfo' m'' ackVer ackErr
|
||||
-- TODO [knocking] why don't we forward these messages?
|
||||
XGrpDirectInv connReq mContent_ msgScope -> memberCanSend (Just m'') msgScope $ Nothing <$ xGrpDirectInv gInfo' m'' conn' connReq mContent_ msg brokerTs
|
||||
XGrpMsgForward fwd msg' -> Nothing <$ xGrpMsgForward gInfo' Nothing m'' fwd (ParsedMsg Nothing Nothing msg') brokerTs
|
||||
@@ -1126,7 +1140,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
checkSndInlineFTComplete conn msgId
|
||||
updateGroupItemsStatus gInfo m conn msgId GSSSent (Just $ isJust proxy)
|
||||
when continued $ do
|
||||
when (isUserGrpFwdRelay gInfo) $ forwardCachedRoster user gInfo m -- roster ahead of the resumed backlog
|
||||
when (isUserGrpFwdRelay gInfo) $ forwardGroupRoster user gInfo m -- roster ahead of the resumed backlog
|
||||
sendPendingGroupMessages user gInfo m conn
|
||||
SWITCH qd phase cStats -> do
|
||||
toView $ CEvtGroupMemberSwitch user gInfo m (SwitchProgress qd phase cStats)
|
||||
@@ -1179,9 +1193,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
CFGetRelayDataJoin -> do
|
||||
-- Update relay member with key, memberId and profile from link
|
||||
relayLinkData_ <- liftIO $ decodeLinkUserData cData
|
||||
case (relayLinkData_, linkEntityId) of
|
||||
(Just RelayShortLinkData {relayProfile = p}, Just entityId) ->
|
||||
relayMemberId <- case (relayLinkData_, linkEntityId) of
|
||||
(Just RelayShortLinkData {relayProfile = p}, Just entityId) -> do
|
||||
withStore $ \db -> updateRelayMemberData db user m (MemberId entityId) (MemberKey relayKey) p
|
||||
pure $ MemberId entityId
|
||||
_ -> throwChatError $ CEException "relay link: no relay link data or entity id"
|
||||
case cReq of
|
||||
CRContactUri crData@ConnReqUriData {crClientData} -> do
|
||||
@@ -1194,13 +1209,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
cReqHash = contactCReqHash $ CRContactUri crData {crScheme = SSSimplex}
|
||||
-- Update connection with data derived from cReq, now available after getConnShortLinkAsync
|
||||
withStore' $ \db -> updateConnLinkData db user conn cReq cReqHash groupLinkId chatV pqSup
|
||||
let GroupMember {memberId = membershipMemId} = membership
|
||||
incognitoProfile = fromLocalProfile <$> incognitoMembershipProfile gInfo
|
||||
let incognitoProfile = fromLocalProfile <$> incognitoMembershipProfile gInfo
|
||||
profileToSend = userProfileInGroup user gInfo incognitoProfile
|
||||
memberPubKey <- case groupKeys gInfo of
|
||||
Just GroupKeys {memberPrivKey} -> pure $ C.publicKey memberPrivKey
|
||||
Nothing -> throwChatError $ CEInternalError "no group keys for channel membership"
|
||||
dm <- encodeConnInfo $ XMember profileToSend membershipMemId (MemberKey memberPubKey)
|
||||
dm <- encodeXMemberConnInfo gInfo relayMemberId profileToSend
|
||||
subMode <- chatReadVar subscriptionMode
|
||||
void $ joinAgentConnectionAsync user (Just conn) True cReq dm subMode
|
||||
CFGetRelayDataAccept -> do
|
||||
@@ -1210,13 +1221,12 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
relayProfile <- liftIO (decodeLinkUserData cData) >>= \case
|
||||
Just RelayShortLinkData {relayProfile = p} -> pure p
|
||||
Nothing -> throwChatError $ CEException "relay link: no relay link data"
|
||||
(confId, m', relay) <- withStore $ \db -> do
|
||||
confId <- withStore $ \db -> do
|
||||
confId <- getRelayConfId db m
|
||||
liftIO $ updateGroupMemberStatus db userId m GSMemAccepted
|
||||
(m', relay) <- setRelayLinkAccepted db vr user m (MemberKey relayKey) relayProfile
|
||||
pure (confId, m', relay)
|
||||
void $ setRelayKey db vr user m (MemberKey relayKey) relayProfile
|
||||
pure confId
|
||||
allowAgentConnectionAsync user conn confId XOk
|
||||
toView $ CEvtGroupRelayUpdated user gInfo m' relay
|
||||
else
|
||||
-- TODO [relays] owner: TBC failed RelayStatus?
|
||||
messageError "relay link: relay member ID mismatch"
|
||||
@@ -1224,7 +1234,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
QCONT -> do
|
||||
continued <- continueSending connEntity conn
|
||||
when continued $ do
|
||||
when (isUserGrpFwdRelay gInfo) $ forwardCachedRoster user gInfo m -- roster ahead of the resumed backlog
|
||||
when (isUserGrpFwdRelay gInfo) $ forwardGroupRoster user gInfo m -- roster ahead of the resumed backlog
|
||||
sendPendingGroupMessages user gInfo m conn
|
||||
MWARN msgId err -> do
|
||||
withStore' $ \db -> updateGroupItemsErrorStatus db msgId (groupMemberId' m) (GSSWarning $ agentSndError err)
|
||||
@@ -1335,10 +1345,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
processContactConnMessage :: AEvent e -> ConnectionEntity -> Connection -> UserContact -> CM ()
|
||||
processContactConnMessage agentMsg connEntity conn UserContact {userContactLinkId = uclId, groupId = ucGroupId_} = case agentMsg of
|
||||
REQ invId pqSupport _ connInfo -> do
|
||||
ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo
|
||||
(signedMsg_, ChatMessage {chatVRange, chatMsgEvent}) <- parseChatMessage' conn connInfo
|
||||
case chatMsgEvent of
|
||||
XContact p xContactId_ welcomeMsgId_ requestMsg_ -> profileContactRequest invId chatVRange p xContactId_ welcomeMsgId_ requestMsg_ pqSupport
|
||||
XMember p joiningMemberId joiningMemberKey -> memberJoinRequestViaRelay invId chatVRange p joiningMemberId joiningMemberKey
|
||||
XMember p joiningMemberId joiningMemberKey viaRelay -> memberJoinRequestViaRelay invId chatVRange signedMsg_ p joiningMemberId joiningMemberKey viaRelay
|
||||
XInfo p -> profileContactRequest invId chatVRange p Nothing Nothing Nothing pqSupport
|
||||
XGrpRelayInv groupRelayInv -> xGrpRelayInv invId chatVRange groupRelayInv
|
||||
XGrpRelayTest challenge _ -> xGrpRelayTest invId chatVRange challenge
|
||||
@@ -1560,7 +1570,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
messageError "processContactConnMessage: chat version range incompatible for accepting group join request"
|
||||
| otherwise -> do
|
||||
let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo
|
||||
mem <- acceptGroupJoinRequestAsync user uclId gInfo invId chatVRange p xContactId_ Nothing welcomeMsgId_ acceptance useRole profileMode Nothing
|
||||
mem <- acceptGroupJoinRequestAsync user uclId gInfo invId chatVRange p xContactId_ Nothing welcomeMsgId_ acceptance useRole profileMode Nothing Nothing
|
||||
(gInfo', mem', scopeInfo) <- mkGroupChatScope gInfo mem
|
||||
createInternalChatItem user (CDGroupRcv gInfo' scopeInfo mem') (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing
|
||||
toView $ CEvtAcceptingGroupJoinRequestMember user gInfo' mem'
|
||||
@@ -1596,19 +1606,37 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
Connection {connId = testCId} <- createRelayTestConnection db vr user acId ConnAccepted chatV subMode
|
||||
liftIO $ setCommandConnId db user cmdId testCId
|
||||
-- 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
|
||||
memberJoinRequestViaRelay :: InvitationId -> VersionRangeChat -> Maybe SignedMsg -> Profile -> MemberId -> MemberKey -> Maybe MemberId -> CM ()
|
||||
memberJoinRequestViaRelay invId chatVRange signedMsg_ p joiningMemberId joiningMemberKey@(MemberKey joiningKey) viaRelay = do
|
||||
(_ucl, gLinkInfo_) <- withStore $ \db -> getUserContactLinkById db userId uclId
|
||||
case gLinkInfo_ of
|
||||
Just GroupLinkInfo {groupId, memberRole = gLinkMemRole} -> do
|
||||
gInfo <- withStore $ \db -> getGroupInfo db vr user groupId
|
||||
mem <- acceptGroupJoinRequestAsync user uclId gInfo invId chatVRange p Nothing (Just joiningMemberId) Nothing GAAccepted gLinkMemRole Nothing (Just joiningMemberKey)
|
||||
existing_ <- withStore' $ \db -> eitherToMaybe <$> runExceptT (getGroupMemberByMemberId db vr user gInfo joiningMemberId)
|
||||
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
|
||||
then acceptJoin gInfo (Just rosterMem) (memberRole' rosterMem)
|
||||
else messageError "memberJoinRequestViaRelay: rejected join claiming privileged memberId (key mismatch or invalid signature)"
|
||||
_ -> acceptJoin gInfo Nothing gLinkMemRole
|
||||
Nothing ->
|
||||
messageError "memberJoinRequestViaRelay: no group link info for relay link"
|
||||
where
|
||||
-- replay defense: the viaRelay == own memberId check (viaRelay is in the signed body); without it a sibling relay could replay a privileged member's signed join
|
||||
verifyKey gInfo rosterMem = case (signedMsg_, groupKeys gInfo) of
|
||||
(Just SignedMsg {chatBinding = CBGroup, signatures, signedBody}, Just GroupKeys {publicGroupId}) ->
|
||||
memberPubKey rosterMem == Just joiningKey
|
||||
&& verifyGroupSig joiningKey publicGroupId joiningMemberId signatures signedBody
|
||||
&& viaRelay == Just (memberId' (membership gInfo))
|
||||
_ -> False
|
||||
acceptJoin gInfo existingMem_ acceptRole = do
|
||||
mem <- acceptGroupJoinRequestAsync user uclId gInfo invId chatVRange p Nothing (Just joiningMemberId) Nothing GAAccepted acceptRole Nothing (Just joiningMemberKey) existingMem_
|
||||
(gInfo', mem', scopeInfo) <- mkGroupChatScope gInfo mem
|
||||
createInternalChatItem user (CDGroupRcv gInfo' scopeInfo mem') (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing
|
||||
toView $ CEvtAcceptingGroupJoinRequestMember user gInfo' mem'
|
||||
Nothing ->
|
||||
messageError "memberJoinRequestViaRelay: no group link info for relay link"
|
||||
|
||||
muteEventInChannel :: GroupInfo -> GroupMember -> Bool
|
||||
muteEventInChannel gInfo@GroupInfo {membership} m =
|
||||
@@ -3176,18 +3204,28 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
validRoster = validateGroupRoster roster
|
||||
relayApplyRoster :: CM (Maybe DeliveryJobScope)
|
||||
relayApplyRoster
|
||||
| maybe False (newVer <=) (rosterVersion gInfo) = Nothing <$ messageWarning "x.grp.roster: not newer than cached version"
|
||||
| maybe False (newVer <=) (rosterVersion gInfo) = Nothing <$ messageWarning "x.grp.roster: not newer than saved version"
|
||||
| otherwise = case verifiedMsg of
|
||||
VMSigned _ sm _ -> do
|
||||
defaultRole <- unknownMemberRole gInfo
|
||||
results <- withStore $ \db -> do
|
||||
res <- processRoster db defaultRole
|
||||
liftIO $ setCachedGroupRoster db gInfo newVer (groupMemberId' author) brokerTs sm
|
||||
pure res
|
||||
emitRosterResults results
|
||||
-- always broadcast on a bump: self-healing, and demotions must reach members
|
||||
pure $ Just DJSGroup {jobSpec = DJDeliveryJob {includePending = False}}
|
||||
VMSigned _ sm _ ->
|
||||
tryAllErrors (setRoster sm) >>= \case
|
||||
Right results -> do
|
||||
emitRosterResults results
|
||||
-- ack only while still setting up (own status RSAccepted); a serving relay (RSActive) must not ack roster broadcasts
|
||||
when (relayOwnStatus gInfo == Just RSAccepted) $ sendRosterAck author newVer Nothing
|
||||
-- always broadcast on a bump: self-healing, and demotions must reach members
|
||||
pure $ Just DJSGroup {jobSpec = DJDeliveryJob {includePending = False}}
|
||||
Left e -> do
|
||||
eToView e
|
||||
when (relayOwnStatus gInfo == Just RSAccepted) $ sendRosterAck author newVer (Just "relay could not save the roster")
|
||||
pure Nothing
|
||||
VMUnsigned _ -> Nothing <$ messageWarning "x.grp.roster: unsigned roster"
|
||||
setRoster :: SignedMsg -> CM ([MemberId], [(GroupMember, GroupMemberRole)])
|
||||
setRoster sm = do
|
||||
defaultRole <- unknownMemberRole gInfo
|
||||
withStore $ \db -> do
|
||||
res <- processRoster db defaultRole
|
||||
liftIO $ setGroupRoster db gInfo newVer (groupMemberId' author) brokerTs sm
|
||||
pure res
|
||||
memberApplyRoster :: CM ()
|
||||
memberApplyRoster
|
||||
| maybe False (newVer <) (rosterVersion gInfo) = messageWarning "x.grp.roster: older than accepted version"
|
||||
@@ -3235,6 +3273,28 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
(gInfo', author', scopeInfo) <- mkGroupChatScope gInfo author
|
||||
createInternalChatItem user (CDGroupRcv gInfo' scopeInfo author') (CIRcvGroupEvent gEvent) (Just brokerTs)
|
||||
toView CEvtMemberRole {user, groupInfo = gInfo', byMember = author', member, fromRole, toRole, msgSigned = Just MSSVerified}
|
||||
sendRosterAck :: GroupMember -> VersionRoster -> Maybe Text -> CM ()
|
||||
sendRosterAck owner ackVer err = void $ sendGroupMessage' user gInfo [owner] (XGrpRosterAck ackVer err)
|
||||
|
||||
xGrpRosterAck :: GroupInfo -> GroupMember -> VersionRoster -> Maybe Text -> CM ()
|
||||
xGrpRosterAck gInfo m ackVer err = do
|
||||
relay_ <- withStore' $ \db -> eitherToMaybe <$> runExceptT (getGroupRelayByGMId db (groupMemberId' m))
|
||||
case relay_ of
|
||||
Just relay@GroupRelay {relayStatus = RSInvited} -> case err of
|
||||
Nothing
|
||||
| rosterVersion gInfo == Just ackVer -> do
|
||||
(relay', gLink) <- withStore $ \db -> do
|
||||
relay' <- liftIO $ updateRelayStatus db relay RSAccepted
|
||||
gLink <- getGroupLink db user gInfo
|
||||
pure (relay', gLink)
|
||||
setGroupLinkDataAsync user gInfo gLink
|
||||
toView $ CEvtGroupRelayUpdated user gInfo m relay'
|
||||
| otherwise -> messageWarning "x.grp.roster.ack: stale version, awaiting ack for the current roster"
|
||||
Just e -> do
|
||||
relay' <- withStore' $ \db -> updateRelayStatusFromTo db relay RSInvited RSRejected
|
||||
toView $ CEvtGroupRelayUpdated user gInfo m relay'
|
||||
messageError $ "x.grp.roster.ack: relay could not save roster, marked rejected: " <> e
|
||||
_ -> pure ()
|
||||
|
||||
checkHostRole :: GroupMember -> GroupMemberRole -> CM ()
|
||||
checkHostRole GroupMember {memberRole, localDisplayName} memRole =
|
||||
@@ -3591,8 +3651,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
| GroupMember {memberPubKey = Just pubKey, memberId} <- member ->
|
||||
case chatBinding of
|
||||
CBGroup | Just GroupKeys {publicGroupId} <- groupKeys gInfo ->
|
||||
let prefix = smpEncode chatBinding <> smpEncode (publicGroupId, memberId)
|
||||
in signed MSSVerified <$ guard (all (\(MsgSignature KRMember sig) -> C.verify (C.APublicVerifyKey C.SEd25519 pubKey) sig (prefix <> signedBody)) signatures)
|
||||
signed MSSVerified <$ guard (verifyGroupSig pubKey publicGroupId memberId signatures signedBody)
|
||||
_ -> signed MSSSignedNoKey <$ guard signatureOptional
|
||||
| otherwise -> signed MSSSignedNoKey <$ guard (signatureOptional || unverifiedAllowed membership member tag)
|
||||
where
|
||||
@@ -3854,10 +3913,15 @@ runDeliveryJobWorker a deliveryKey Worker {doWork} = do
|
||||
bucketSize <- asks $ deliveryBucketSize . config
|
||||
senders <- withStore' $ \db ->
|
||||
fmap catMaybes . forM senderGMIds $ \sId ->
|
||||
fmap eitherToMaybe . runExceptT $ do
|
||||
fmap (join . eitherToMaybe) . runExceptT $ do
|
||||
sender <- getNonRemovedMemberById db vr user sId
|
||||
vec <- getMemberRelationsVector db sender
|
||||
pure (sender, vec)
|
||||
-- owners are already known to every member (group link + owner-intro in introduceInChannel),
|
||||
-- so we never disseminate their profile (redundant, and races with joins re-announcing the owner)
|
||||
if memberRole' sender == GROwner
|
||||
then pure Nothing
|
||||
else do
|
||||
vec <- getMemberRelationsVector db sender
|
||||
pure $ Just (sender, vec)
|
||||
let missingSenders = length senderGMIds - length senders
|
||||
when (missingSenders > 0) $
|
||||
logInfo $ "delivery job " <> tshow jobId <> ": " <> tshow missingSenders <> " senders missing; skipping their profile prepend"
|
||||
|
||||
@@ -448,6 +448,11 @@ data MsgSigning = MsgSigning
|
||||
encodeChatBinding :: ChatBinding -> ByteString -> ByteString
|
||||
encodeChatBinding cb bindingData = smpEncode cb <> bindingData
|
||||
|
||||
signChatMsgBody :: MsgSigning -> ByteString -> SignedMsg
|
||||
signChatMsgBody MsgSigning {bindingTag, bindingData, keyRef, privKey} msgBody =
|
||||
let sig = C.ASignature C.SEd25519 $ C.sign' privKey (encodeChatBinding bindingTag bindingData <> msgBody)
|
||||
in SignedMsg {chatBinding = bindingTag, signatures = MsgSignature keyRef sig L.:| [], signedBody = msgBody}
|
||||
|
||||
data ChatMsgEvent (e :: MsgEncoding) where
|
||||
XMsgNew :: MsgContainer -> ChatMsgEvent 'Json
|
||||
XMsgFileDescr :: {msgId :: SharedMsgId, fileDescr :: FileDescr} -> ChatMsgEvent 'Json
|
||||
@@ -461,7 +466,7 @@ data ChatMsgEvent (e :: MsgEncoding) where
|
||||
XFileCancel :: SharedMsgId -> ChatMsgEvent 'Json
|
||||
XInfo :: Profile -> ChatMsgEvent 'Json
|
||||
XContact :: {profile :: Profile, contactReqId :: Maybe XContactId, welcomeMsgId :: Maybe SharedMsgId, requestMsg :: Maybe (SharedMsgId, MsgContent)} -> ChatMsgEvent 'Json
|
||||
XMember :: {profile :: Profile, newMemberId :: MemberId, newMemberKey :: MemberKey} -> ChatMsgEvent 'Json
|
||||
XMember :: {profile :: Profile, newMemberId :: MemberId, newMemberKey :: MemberKey, viaRelay :: Maybe MemberId} -> ChatMsgEvent 'Json
|
||||
XDirectDel :: ChatMsgEvent 'Json
|
||||
XGrpInv :: GroupInvitation -> ChatMsgEvent 'Json
|
||||
XGrpAcpt :: MemberId -> ChatMsgEvent 'Json
|
||||
@@ -491,6 +496,7 @@ data ChatMsgEvent (e :: MsgEncoding) where
|
||||
XGrpPrefs :: GroupPreferences -> ChatMsgEvent 'Json
|
||||
XGrpDirectInv :: ConnReqInvitation -> Maybe MsgContent -> Maybe MsgScope -> ChatMsgEvent 'Json
|
||||
XGrpRoster :: GroupRoster -> ChatMsgEvent 'Json
|
||||
XGrpRosterAck :: VersionRoster -> Maybe Text -> ChatMsgEvent 'Json
|
||||
XGrpMsgForward :: GrpMsgForward -> ChatMessage 'Json -> ChatMsgEvent 'Json
|
||||
XInfoProbe :: Probe -> ChatMsgEvent 'Json
|
||||
XInfoProbeCheck :: ProbeHash -> ChatMsgEvent 'Json
|
||||
@@ -1048,6 +1054,7 @@ data CMEventTag (e :: MsgEncoding) where
|
||||
XGrpPrefs_ :: CMEventTag 'Json
|
||||
XGrpDirectInv_ :: CMEventTag 'Json
|
||||
XGrpRoster_ :: CMEventTag 'Json
|
||||
XGrpRosterAck_ :: CMEventTag 'Json
|
||||
XGrpMsgForward_ :: CMEventTag 'Json
|
||||
XInfoProbe_ :: CMEventTag 'Json
|
||||
XInfoProbeCheck_ :: CMEventTag 'Json
|
||||
@@ -1109,6 +1116,7 @@ instance MsgEncodingI e => StrEncoding (CMEventTag e) where
|
||||
XGrpPrefs_ -> "x.grp.prefs"
|
||||
XGrpDirectInv_ -> "x.grp.direct.inv"
|
||||
XGrpRoster_ -> "x.grp.roster"
|
||||
XGrpRosterAck_ -> "x.grp.roster.ack"
|
||||
XGrpMsgForward_ -> "x.grp.msg.forward"
|
||||
XInfoProbe_ -> "x.info.probe"
|
||||
XInfoProbeCheck_ -> "x.info.probe.check"
|
||||
@@ -1171,6 +1179,7 @@ instance StrEncoding ACMEventTag where
|
||||
"x.grp.prefs" -> XGrpPrefs_
|
||||
"x.grp.direct.inv" -> XGrpDirectInv_
|
||||
"x.grp.roster" -> XGrpRoster_
|
||||
"x.grp.roster.ack" -> XGrpRosterAck_
|
||||
"x.grp.msg.forward" -> XGrpMsgForward_
|
||||
"x.info.probe" -> XInfoProbe_
|
||||
"x.info.probe.check" -> XInfoProbeCheck_
|
||||
@@ -1229,6 +1238,7 @@ toCMEventTag msg = case msg of
|
||||
XGrpPrefs _ -> XGrpPrefs_
|
||||
XGrpDirectInv {} -> XGrpDirectInv_
|
||||
XGrpRoster _ -> XGrpRoster_
|
||||
XGrpRosterAck {} -> XGrpRosterAck_
|
||||
XGrpMsgForward {} -> XGrpMsgForward_
|
||||
XInfoProbe _ -> XInfoProbe_
|
||||
XInfoProbeCheck _ -> XInfoProbeCheck_
|
||||
@@ -1356,7 +1366,7 @@ appJsonToCM AppMessageJson {v, msgId, event, params} = do
|
||||
reqContent <- opt "content"
|
||||
let requestMsg = (,) <$> reqMsgId <*> reqContent
|
||||
pure XContact {profile, contactReqId, welcomeMsgId, requestMsg}
|
||||
XMember_ -> XMember <$> p "profile" <*> p "newMemberId" <*> p "newMemberKey"
|
||||
XMember_ -> XMember <$> p "profile" <*> p "newMemberId" <*> p "newMemberKey" <*> opt "viaRelay"
|
||||
XDirectDel_ -> pure XDirectDel
|
||||
XGrpInv_ -> XGrpInv <$> p "groupInvitation"
|
||||
XGrpAcpt_ -> XGrpAcpt <$> p "memberId"
|
||||
@@ -1389,6 +1399,7 @@ appJsonToCM AppMessageJson {v, msgId, event, params} = do
|
||||
XGrpPrefs_ -> XGrpPrefs <$> p "groupPreferences"
|
||||
XGrpDirectInv_ -> XGrpDirectInv <$> p "connReq" <*> opt "content" <*> opt "scope"
|
||||
XGrpRoster_ -> XGrpRoster <$> (GroupRoster <$> p "version" <*> p "roster")
|
||||
XGrpRosterAck_ -> XGrpRosterAck <$> p "version" <*> opt "error"
|
||||
XGrpMsgForward_ -> do
|
||||
fwdSender <- opt "memberId" >>= \case
|
||||
Just memberId -> FwdMember memberId . fromMaybe "" <$> opt "memberName"
|
||||
@@ -1430,7 +1441,7 @@ chatToAppMessage chatMsg@ChatMessage {chatVRange, msgId, chatMsgEvent} = case en
|
||||
XFileCancel sharedMsgId -> o ["msgId" .= sharedMsgId]
|
||||
XInfo profile -> o ["profile" .= profile]
|
||||
XContact {profile, contactReqId, welcomeMsgId, requestMsg} -> o $ ("contactReqId" .=? contactReqId) $ ("welcomeMsgId" .=? welcomeMsgId) $ ("msgId" .=? (fst <$> requestMsg)) $ ("content" .=? (snd <$> requestMsg)) $ ["profile" .= profile]
|
||||
XMember {profile, newMemberId, newMemberKey} -> o ["profile" .= profile, "newMemberId" .= newMemberId, "newMemberKey" .= newMemberKey]
|
||||
XMember {profile, newMemberId, newMemberKey, viaRelay} -> o $ ("viaRelay" .=? viaRelay) ["profile" .= profile, "newMemberId" .= newMemberId, "newMemberKey" .= newMemberKey]
|
||||
XDirectDel -> JM.empty
|
||||
XGrpInv groupInv -> o ["groupInvitation" .= groupInv]
|
||||
XGrpAcpt memId -> o ["memberId" .= memId]
|
||||
@@ -1462,6 +1473,7 @@ chatToAppMessage chatMsg@ChatMessage {chatVRange, msgId, chatMsgEvent} = case en
|
||||
XGrpPrefs p -> o ["groupPreferences" .= p]
|
||||
XGrpDirectInv connReq content scope -> o $ ("content" .=? content) $ ("scope" .=? scope) ["connReq" .= connReq]
|
||||
XGrpRoster GroupRoster {version, roster} -> o ["version" .= version, "roster" .= roster]
|
||||
XGrpRosterAck version err -> o $ ("error" .=? err) ["version" .= version]
|
||||
XGrpMsgForward GrpMsgForward {fwdSender, fwdBrokerTs} msg -> o $ encodeFwdSender fwdSender ["msg" .= msg, "msgTs" .= fwdBrokerTs]
|
||||
where
|
||||
encodeFwdSender = \case
|
||||
|
||||
@@ -87,15 +87,15 @@ module Simplex.Chat.Store.Groups
|
||||
getGroupRelays,
|
||||
getConnectedGroupRelays,
|
||||
setGroupRosterVersion,
|
||||
setCachedGroupRoster,
|
||||
getCachedGroupRoster,
|
||||
setGroupRoster,
|
||||
getGroupRoster,
|
||||
setGroupMemberKeyRole,
|
||||
createRelayForOwner,
|
||||
getCreateRelayForMember,
|
||||
createRelayConnection,
|
||||
updateRelayStatus,
|
||||
updateRelayStatusFromTo,
|
||||
setRelayLinkAccepted,
|
||||
setRelayKey,
|
||||
setRelayLinkConfId,
|
||||
updateRelayCapabilities,
|
||||
getRelayConfId,
|
||||
@@ -362,6 +362,7 @@ createNewGroup db vr user@User {userId} groupProfile incognitoProfile useRelays
|
||||
Just PublicGroupProfile {groupType, groupLink, publicGroupId} -> (Just groupType, Just groupLink, Just publicGroupId)
|
||||
Nothing -> (Nothing, Nothing, Nothing)
|
||||
fullGroupPreferences = mergeGroupPreferences groupPreferences
|
||||
rosterVersion0 = if useRelays then Just (VersionRoster 0) else Nothing
|
||||
currentTs <- getCurrentTime
|
||||
customUserProfileId <- mapM (createIncognitoProfile_ db userId currentTs) incognitoProfile
|
||||
withLocalDisplayName db userId displayName $ \ldn -> runExceptT $ do
|
||||
@@ -392,11 +393,11 @@ createNewGroup db vr user@User {userId} groupProfile incognitoProfile useRelays
|
||||
INSERT INTO groups
|
||||
(use_relays, creating_in_progress, local_display_name, user_id, group_profile_id, enable_ntfs,
|
||||
created_at, updated_at, chat_ts, user_member_profile_sent_at,
|
||||
root_priv_key, root_pub_key, member_priv_key, public_member_count)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
root_priv_key, root_pub_key, member_priv_key, public_member_count, roster_version)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
|]
|
||||
( (BI useRelays, BI useRelays, ldn, userId, profileId, BI True, currentTs, currentTs, currentTs, currentTs)
|
||||
:. (rootPrivKey_, rootPubKey_, memberPrivKey_, publicMemberCount_)
|
||||
:. (rootPrivKey_, rootPubKey_, memberPrivKey_, publicMemberCount_, rosterVersion0)
|
||||
)
|
||||
insertedRowId db
|
||||
let memberPubKey = C.publicKey . memberPrivKey <$> groupKeys
|
||||
@@ -423,7 +424,7 @@ createNewGroup db vr user@User {userId} groupProfile incognitoProfile useRelays
|
||||
chatItemTTL = Nothing,
|
||||
uiThemes = Nothing,
|
||||
groupSummary = GroupSummary {currentMembers = 1, publicMemberCount = publicMemberCount_},
|
||||
rosterVersion = Nothing,
|
||||
rosterVersion = rosterVersion0,
|
||||
customData = Nothing,
|
||||
membersRequireAttention = 0,
|
||||
viaGroupLinkUri = Nothing,
|
||||
@@ -1210,7 +1211,7 @@ getGroupModerators db vr user@User {userId, userContactId} GroupInfo {groupId} =
|
||||
|
||||
-- Moderators and admins only, excluding owners and non-current members.
|
||||
-- Used for roster-related paths where owners must not be touched (owners are
|
||||
-- link-anchored), and left/removed members must not appear in the cached roster.
|
||||
-- link-anchored), and left/removed members must not appear in the saved roster.
|
||||
getGroupRosterMembers :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> IO [GroupMember]
|
||||
getGroupRosterMembers db vr user@User {userId, userContactId} GroupInfo {groupId} = do
|
||||
filter memberCurrent . map (toContactMember vr user)
|
||||
@@ -1410,9 +1411,9 @@ setGroupRosterVersion db GroupInfo {groupId} v = do
|
||||
currentTs <- getCurrentTime
|
||||
DB.execute db "UPDATE groups SET roster_version = ?, updated_at = ? WHERE group_id = ?" (v, currentTs, groupId)
|
||||
|
||||
-- Relay caches the verbatim signed roster (parts + sending owner + broker ts) to re-forward to joiners.
|
||||
setCachedGroupRoster :: DB.Connection -> GroupInfo -> VersionRoster -> GroupMemberId -> UTCTime -> SignedMsg -> IO ()
|
||||
setCachedGroupRoster db GroupInfo {groupId} v ownerGMId brokerTs SignedMsg {chatBinding, signatures, signedBody} = do
|
||||
-- Relay saves the verbatim signed roster (parts + sending owner + broker ts) to re-forward to joiners.
|
||||
setGroupRoster :: DB.Connection -> GroupInfo -> VersionRoster -> GroupMemberId -> UTCTime -> SignedMsg -> IO ()
|
||||
setGroupRoster db GroupInfo {groupId} v ownerGMId brokerTs SignedMsg {chatBinding, signatures, signedBody} = do
|
||||
currentTs <- getCurrentTime
|
||||
DB.execute
|
||||
db
|
||||
@@ -1424,8 +1425,8 @@ setCachedGroupRoster db GroupInfo {groupId} v ownerGMId brokerTs SignedMsg {chat
|
||||
|]
|
||||
((v, ownerGMId, brokerTs, chatBinding) :. (Binary (smpEncode signatures), Binary signedBody, currentTs, groupId))
|
||||
|
||||
getCachedGroupRoster :: DB.Connection -> GroupInfo -> IO (Maybe (GroupMemberId, UTCTime, SignedMsg))
|
||||
getCachedGroupRoster db GroupInfo {groupId} =
|
||||
getGroupRoster :: DB.Connection -> GroupInfo -> IO (Maybe (GroupMemberId, UTCTime, SignedMsg))
|
||||
getGroupRoster db GroupInfo {groupId} =
|
||||
(>>= toRoster)
|
||||
<$> maybeFirstRow
|
||||
id
|
||||
@@ -1539,18 +1540,10 @@ 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 -> VersionRangeChat -> User -> GroupMember -> MemberKey -> Profile -> ExceptT StoreError IO (GroupMember, GroupRelay)
|
||||
setRelayLinkAccepted db vr user m (MemberKey relayKey) profile = do
|
||||
setRelayKey :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> MemberKey -> Profile -> ExceptT StoreError IO (GroupMember, GroupRelay)
|
||||
setRelayKey db vr user m (MemberKey relayKey) profile = do
|
||||
let gmId = groupMemberId' m
|
||||
currentTs <- liftIO getCurrentTime
|
||||
liftIO $ DB.execute
|
||||
db
|
||||
[sql|
|
||||
UPDATE group_relays
|
||||
SET relay_status = ?, updated_at = ?
|
||||
WHERE group_member_id = ?
|
||||
|]
|
||||
(RSAccepted, currentTs, gmId)
|
||||
liftIO $ DB.execute
|
||||
db
|
||||
[sql|
|
||||
|
||||
@@ -237,10 +237,7 @@ createNewSndMessage db gVar connOrGroupId chatMsgEvent msgSigning_ encodeMessage
|
||||
case encodeMessage (SharedMsgId sharedMsgId) of
|
||||
ECMLarge -> pure $ Left SELargeMsg
|
||||
ECMEncoded msgBody -> do
|
||||
let signedMsg_ = signBody <$> msgSigning_
|
||||
signBody MsgSigning {bindingTag, bindingData, keyRef, privKey} =
|
||||
let sig = C.ASignature C.SEd25519 $ C.sign' privKey (encodeChatBinding bindingTag bindingData <> msgBody)
|
||||
in SignedMsg {chatBinding = bindingTag, signatures = MsgSignature keyRef sig :| [], signedBody = msgBody}
|
||||
let signedMsg_ = (`signChatMsgBody` msgBody) <$> msgSigning_
|
||||
createdAt <- getCurrentTime
|
||||
DB.execute
|
||||
db
|
||||
|
||||
@@ -23,7 +23,7 @@ import qualified Data.ByteString.Char8 as B
|
||||
import Data.Maybe (fromMaybe, isJust, maybeToList)
|
||||
import Data.Time (UTCTime)
|
||||
import Data.Int (Int64)
|
||||
import Data.List (intercalate, isInfixOf)
|
||||
import Data.List (intercalate, isInfixOf, isSuffixOf)
|
||||
import qualified Data.Map.Strict as M
|
||||
import qualified Data.Text as T
|
||||
import Simplex.Chat.Controller (ChatConfig (..), ChatHooks (..), ChatLogLevel (..), defaultChatHooks)
|
||||
@@ -268,11 +268,6 @@ chatGroupTests = do
|
||||
it "should update channel preferences (signed)" testChannelUpdatePrefsSigned
|
||||
it "should change member role (signed)" testChannelChangeRoleSigned
|
||||
it "should block member for all (signed)" testChannelBlockMemberSigned
|
||||
it "moderator action verifies via owner-signed roster" testChannelModeratorActionViaRoster
|
||||
it "removed moderator drops from the roster cache" testChannelRemovedModeratorRefreshesRoster
|
||||
it "leaving moderator drops from the roster cache" testChannelLeftModeratorDropsFromRoster
|
||||
it "role transitions update the roster (mod <-> admin, admin -> non-roster)" testChannelRoleTransitionsUpdateRoster
|
||||
it "malicious relay cannot downgrade or re-key a roster-established moderator via XGrpMemNew" testChannelRelayCannotDowngradeRosterMember
|
||||
it "should remove member (signed)" testChannelRemoveMemberSigned
|
||||
it "should delete channel (signed)" testChannelDeleteGroupSigned
|
||||
it "should delete channel and clean up relay connections" testChannelDeleteGroupCleanup
|
||||
@@ -291,6 +286,13 @@ chatGroupTests = do
|
||||
it "operator allow clears rejection and relay accepts again" testRelayAllowAcceptsAgain
|
||||
it "rejection on channel A does not affect unrelated channel B" testRelayDoesNotRejectUnrelatedChannel
|
||||
it "concurrent fresh invitations both rejected" testRelayRejectRaceConcurrentInvitations
|
||||
describe "promoted members roster" $ do
|
||||
it "moderator action verifies via owner-signed roster" testChannelModeratorActionViaRoster
|
||||
it "removed moderator drops from the roster cache" testChannelRemovedModeratorRefreshesRoster
|
||||
it "leaving moderator drops from the roster cache" testChannelLeftModeratorDropsFromRoster
|
||||
it "role transitions update the roster (mod <-> admin, admin -> non-roster)" testChannelRoleTransitionsUpdateRoster
|
||||
it "malicious relay cannot downgrade or re-key a roster-established moderator via XGrpMemNew" testChannelRelayCannotDowngradeRosterMember
|
||||
it "should add relay to channel with roster (relay caches roster before joinable)" testChannelAddRelayWithRoster
|
||||
describe "channel message operations" $ do
|
||||
it "should update channel message" testChannelMessageUpdate
|
||||
it "should delete channel message" testChannelMessageDelete
|
||||
@@ -8694,7 +8696,7 @@ prepareChannel2Relays gName owner relay1 relay2 = do
|
||||
owner <## ("#" <> gName <> ": group link relays updated, current relays:")
|
||||
owner
|
||||
<### [ EndsWith ": active",
|
||||
EndsWith ": accepted"
|
||||
Predicate (\l -> ": invited" `isSuffixOf` l || ": accepted" `isSuffixOf` l)
|
||||
]
|
||||
owner <## "group link:"
|
||||
void $ getTermLine owner -- consume group link line
|
||||
@@ -10336,6 +10338,65 @@ testChannelAddRelay ps =
|
||||
[bob, cath] *<# "#team> hello"
|
||||
[dan, eve] *<# "#team> hello [>>]"
|
||||
|
||||
testChannelAddRelayWithRoster :: HasCallStack => TestParams -> IO ()
|
||||
testChannelAddRelayWithRoster ps =
|
||||
withNewTestChat ps "alice" aliceProfile $ \alice ->
|
||||
withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob ->
|
||||
withNewTestChatOpts ps relayTestOpts "dan" danProfile $ \dan ->
|
||||
withNewTestChat ps "cath" cathProfile $ \cath ->
|
||||
withNewTestChat ps "eve" eveProfile $ \_eve -> do
|
||||
(shortLink, fullLink) <- prepareChannel1Relay "team" alice bob
|
||||
memberJoinChannel "team" [bob] [alice] shortLink fullLink cath
|
||||
|
||||
-- promote cath to moderator: the roster is created (bob caches it)
|
||||
threadDelay 100000
|
||||
alice ##> "/mr #team cath moderator"
|
||||
alice <## "#team: you changed the role of cath to moderator (signed)"
|
||||
concurrentlyN_
|
||||
[ bob <## "#team: alice changed the role of cath from member to moderator (signed)",
|
||||
cath <## "#team: alice changed your role from member to moderator (signed)"
|
||||
]
|
||||
threadDelay 100000
|
||||
|
||||
-- add dan as a 2nd relay; with a roster present it must cache the roster and ack
|
||||
-- (XGrpRosterAck) before alice publishes it as joinable
|
||||
dan ##> "/ad"
|
||||
(danSLink, _cLink) <- getContactLinks dan True
|
||||
alice ##> ("/relays name=dan " <> danSLink)
|
||||
alice <## "ok"
|
||||
alice ##> "/_add relays #1 2"
|
||||
alice <## "#team: group relays:"
|
||||
alice <## " - relay id 1: active"
|
||||
alice <## " - relay id 2: invited"
|
||||
concurrentlyN_
|
||||
[ do
|
||||
alice <## "#team: group link relays updated, current relays:"
|
||||
alice
|
||||
<### [ " - relay id 1: active",
|
||||
" - relay id 2: active"
|
||||
]
|
||||
alice <## "group link:"
|
||||
void $ getTermLine alice,
|
||||
dan <## "#team: you joined the group as relay"
|
||||
]
|
||||
|
||||
-- cath (an existing member) connects to the new relay and is attached to her roster
|
||||
-- record, kept as moderator
|
||||
concurrentlyN_
|
||||
[ do
|
||||
cath <## "#team: joining the group (connecting to relay dan)..."
|
||||
cath <## "#team: you joined the group (connected to relay dan)",
|
||||
dan
|
||||
<### [ EndsWith "changed the role of cath from member to moderator (signed)",
|
||||
EndsWith "accepting request to join group #team...",
|
||||
EndsWith "member cath is connected"
|
||||
]
|
||||
]
|
||||
|
||||
threadDelay 100000
|
||||
-- the new relay holds the roster (cath is moderator) before it serves joiners
|
||||
checkMemberRow dan "cath" (Just "moderator")
|
||||
|
||||
testChannelRemoveRelay :: HasCallStack => TestParams -> IO ()
|
||||
testChannelRemoveRelay ps =
|
||||
withNewTestChat ps "alice" aliceProfile $ \alice ->
|
||||
|
||||
Reference in new issue
Block a user