From 89e2bf9d2dc7632984187bbc413d63e8b3171f5f Mon Sep 17 00:00:00 2001 From: "Evgeny @ SimpleX Chat" <259188159+evgeny-simplex@users.noreply.github.com> Date: Sat, 28 Mar 2026 17:10:12 +0000 Subject: [PATCH] implement group ID --- cabal.project | 2 +- src/Simplex/Chat/Library/Commands.hs | 17 ++++++++++++----- src/Simplex/Chat/Library/Internal.hs | 6 +++--- src/Simplex/Chat/Library/Subscriber.hs | 7 ++++--- src/Simplex/Chat/Store/Groups.hs | 5 +++-- src/Simplex/Chat/Store/Shared.hs | 4 ++-- src/Simplex/Chat/Types.hs | 5 +++-- 7 files changed, 28 insertions(+), 18 deletions(-) diff --git a/cabal.project b/cabal.project index 5b75f49edb..5cfb1d6707 100644 --- a/cabal.project +++ b/cabal.project @@ -21,7 +21,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 782cacfb3cc57883465eecc0b9b30662daf2b81f + tag: 7a01f7ce09382bea60d368f4834f0fd9fcb5036f source-repository-package type: git diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index c9d85b9f45..87d9314e83 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -2024,6 +2024,11 @@ processChatCommand vr nm = \case Nothing -> throwChatError $ CEException "failed to retrieve relays: no short link" (FixedLinkData {linkConnReq = mainCReq@(CRContactUri crData), linkEntityId, rootKey}, cData@(ContactLinkData _ UserContactData {owners, relays})) <- getShortLinkConnReq nm user sLnk groupSLinkData_ <- liftIO $ decodeLinkUserData cData + -- Validate link entity ID matches group profile's sharedGroupId + forM_ groupSLinkData_ $ \GroupShortLinkData {groupProfile = GroupProfile {sharedGroupId = profileId_}} -> + forM_ profileId_ $ \(B64UrlByteString profileId) -> + when (linkEntityId /= Just profileId) $ + throwChatError $ CEException "group link: linkEntityId does not match profile sharedGroupId" let publicGroupData_ = groupSLinkData_ >>= \GroupShortLinkData {publicGroupData} -> publicGroupData publicMemberCount_ = (\PublicGroupData {publicMemberCount} -> publicMemberCount) <$> publicGroupData_ -- Prepare group record once before connecting to relays (updatePreparedRelayedGroup): @@ -2382,11 +2387,13 @@ processChatCommand vr nm = \case prepareGroupLink user = do gVar <- asks random groupLinkId <- GroupLinkId <$> drgRandomBytes 16 - sharedGroupId <- drgRandomBytes 24 subMode <- chatReadVar subscriptionMode - let crClientData = encodeJSON $ CRDataGroup groupLinkId + -- generate root key pair; entity ID = sha256(rootPubKey) — see docs/rfcs/2026-03-28-group-identity-binding.md + rootKey@(rootPubKey, rootPrivKey) <- liftIO $ atomically $ C.generateKeyPair gVar + let sharedGroupId = C.sha256Hash (C.pubKeyBytes rootPubKey) + crClientData = encodeJSON $ CRDataGroup groupLinkId -- prepare link with sharedGroupId as linkEntityId (no server request) - ((_, rootPrivKey), ccLink, preparedParams) <- withAgent $ \a -> prepareConnectionLink a (aUserId user) (Just sharedGroupId) True (Just crClientData) + (ccLink, preparedParams) <- withAgent $ \a -> prepareConnectionLink a (aUserId user) rootKey sharedGroupId True (Just crClientData) ccLink' <- createdChannelLink <$> shortenCreatedLink ccLink sLnk <- case toShortLinkContact ccLink' of Just sl -> pure sl @@ -2394,7 +2401,7 @@ processChatCommand vr nm = \case -- generate owner key, OwnerAuth signed by root key memberId <- MemberId <$> liftIO (encodedRandomBytes gVar 12) (memberPrivKey, ownerAuth) <- liftIO $ SL.newOwnerAuth gVar (unMemberId memberId) rootPrivKey - let groupProfile' = (groupProfile :: GroupProfile) {groupLink = Just sLnk} + let groupProfile' = (groupProfile :: GroupProfile) {groupLink = Just sLnk, sharedGroupId = Just (B64UrlByteString sharedGroupId)} userData = encodeShortLinkData $ GroupShortLinkData {groupProfile = groupProfile', publicGroupData = Just (PublicGroupData 1)} userLinkData = UserContactLinkData UserContactData {direct = False, owners = [ownerAuth], relays = [], userData} -- create connection with prepared link (single network call) @@ -5086,7 +5093,7 @@ chatCommandP = { directMessages = Just DirectMessagesGroupPreference {enable = FEOn, role = Nothing}, history = Just HistoryGroupPreference {enable = FEOn} } - pure GroupProfile {displayName = gName, fullName = "", shortDescr, description = Nothing, image = Nothing, groupLink = Nothing, groupPreferences, memberAdmission = Nothing} + pure GroupProfile {displayName = gName, fullName = "", shortDescr, description = Nothing, image = Nothing, groupLink = Nothing, groupPreferences, memberAdmission = Nothing, sharedGroupId = Nothing} memberCriteriaP = ("all" $> Just MCAll) <|> ("off" $> Nothing) shortDescrP = do descr <- A.takeWhile1 isSpace *> (T.dropWhileEnd isSpace <$> textP) <|> pure "" diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index de8511bce9..20475ca606 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -1055,7 +1055,7 @@ acceptRelayJoinRequestAsync businessGroupProfile :: Profile -> GroupPreferences -> GroupProfile businessGroupProfile Profile {displayName, fullName, shortDescr, image} groupPreferences = - GroupProfile {displayName, fullName, description = Nothing, shortDescr, image, groupLink = Nothing, groupPreferences = Just groupPreferences, memberAdmission = Nothing} + GroupProfile {displayName, fullName, description = Nothing, shortDescr, image, groupLink = Nothing, groupPreferences = Just groupPreferences, memberAdmission = Nothing, sharedGroupId = Nothing} introduceToModerators :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> CM () introduceToModerators vr user gInfo@GroupInfo {groupId} m@GroupMember {memberRole, memberId} = do @@ -1882,9 +1882,9 @@ createSndMessages idsEvents = do encodeChatMessage maxEncodedMsgLength ChatMessage {chatVRange = vr, msgId = Just sharedMsgId, chatMsgEvent = evnt} groupMsgSigning :: GroupInfo -> ChatMsgEvent e -> Maybe MsgSigning -groupMsgSigning gInfo@GroupInfo {membership = GroupMember {memberId}, groupKeys = Just GroupKeys {groupRootKey, memberPrivKey}} evt +groupMsgSigning gInfo@GroupInfo {membership = GroupMember {memberId}, groupKeys = Just GroupKeys {sharedGroupId, memberPrivKey}} evt | useRelays' gInfo && requiresSignature (toCMEventTag evt) = - Just $ MsgSigning CBGroup (smpEncode (groupRootPubKey groupRootKey, memberId)) KRMember memberPrivKey + Just $ MsgSigning CBGroup (smpEncode (sharedGroupId, memberId)) KRMember memberPrivKey groupMsgSigning _ _ = Nothing sendGroupMemberMessages :: forall e. MsgEncodingI e => User -> GroupInfo -> Connection -> NonEmpty (ChatMsgEvent e) -> CM () diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 650b554857..ae8231a7c5 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -3054,8 +3054,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = toView $ CEvtGroupDeleted user gInfo'' {membership = membership {memberStatus = GSMemGroupDeleted}} m' msgSigned xGrpInfo :: GroupInfo -> GroupMember -> GroupProfile -> RcvMessage -> UTCTime -> CM (Maybe DeliveryJobScope) - xGrpInfo g@GroupInfo {groupProfile = p, businessChat} m@GroupMember {memberRole} p' msg@RcvMessage {msgSigned} brokerTs + xGrpInfo g@GroupInfo {groupProfile = p@GroupProfile {sharedGroupId = gId}, businessChat} m@GroupMember {memberRole} p'@GroupProfile {sharedGroupId = gId'} msg@RcvMessage {msgSigned} brokerTs | memberRole < GROwner = messageError "x.grp.info with insufficient member permissions" $> Nothing + | gId' /= gId = messageError "x.grp.info: sharedGroupId cannot be changed" $> Nothing | otherwise = do case businessChat of Nothing -> unless (p == p') $ do @@ -3233,8 +3234,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = Just sm@SignedMsg {chatBinding, signatures, signedBody} | GroupMember {memberPubKey = Just pubKey, memberId} <- member -> case chatBinding of - CBGroup | Just GroupKeys {groupRootKey} <- groupKeys gInfo -> - let prefix = smpEncode chatBinding <> smpEncode (groupRootPubKey groupRootKey, memberId) + CBGroup | Just GroupKeys {sharedGroupId} <- groupKeys gInfo -> + let prefix = smpEncode chatBinding <> smpEncode (sharedGroupId, memberId) in signed MSSVerified <$ guard (all (\(MsgSignature KRMember sig) -> C.verify (C.APublicVerifyKey C.SEd25519 pubKey) sig (prefix <> signedBody)) signatures) _ -> signed MSSSignedNoKey <$ guard signatureOptional | otherwise -> signed MSSSignedNoKey <$ guard (signatureOptional || unverifiedAllowed membership member tag) diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 8698b1718c..53ffbb878e 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -1459,7 +1459,8 @@ createRelayRequestGroup db vr user@User {userId} GroupRelayInvitation {fromMembe image = Nothing, groupLink = Nothing, groupPreferences = Nothing, - memberAdmission = Nothing + memberAdmission = Nothing, + sharedGroupId = Nothing } (groupId, _groupLDN) <- createGroup_ db userId placeholderProfile Nothing Nothing True (Just RSInvited) Nothing currentTs -- Store relay request data for recovery @@ -2227,7 +2228,7 @@ updateGroupProfileFromMember db user g@GroupInfo {groupId} Profile {displayName |] (Only groupId) toGroupProfile (displayName, fullName, shortDescr, description, image, groupLink, groupPreferences, memberAdmission) = - GroupProfile {displayName, fullName, shortDescr, description, image, groupLink, groupPreferences, memberAdmission} + GroupProfile {displayName, fullName, shortDescr, description, image, groupLink, groupPreferences, memberAdmission, sharedGroupId = Nothing} getGroupInfoByUserContactLinkConnReq :: DB.Connection -> VersionRangeChat -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe GroupInfo) getGroupInfoByUserContactLinkConnReq db vr user@User {userId} (cReqSchema1, cReqSchema2) = do diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index 0f25379204..a8f1c5b485 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -676,11 +676,11 @@ toGroupInfo vr userContactId chatTags ((groupId, localDisplayName, displayName, let membership = (toGroupMember userContactId userMemberRow) {memberChatVRange = vr} chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite} fullGroupPreferences = mergeGroupPreferences groupPreferences - groupProfile = GroupProfile {displayName, fullName, shortDescr, description, image, groupPreferences, memberAdmission, groupLink} + groupKeys = toGroupKeys groupKeysRow + groupProfile = GroupProfile {displayName, fullName, shortDescr, description, image, groupPreferences, memberAdmission, groupLink, sharedGroupId = (\GroupKeys {sharedGroupId = s} -> s) <$> groupKeys} businessChat = toBusinessChatInfo businessRow preparedGroup = toPreparedGroup preparedGroupRow groupSummary = GroupSummary {currentMembers, publicMemberCount} - groupKeys = toGroupKeys groupKeysRow in GroupInfo {groupId, useRelays = BoolDef useRelays, relayOwnStatus, localDisplayName, groupProfile, localAlias, businessChat, fullGroupPreferences, membership, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, preparedGroup, chatTags, chatItemTTL, uiThemes, groupSummary, customData, membersRequireAttention, viaGroupLinkUri, groupKeys} toPreparedGroup :: PreparedGroupRow -> Maybe PreparedGroup diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 32d1c726bf..584e46a226 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -151,7 +151,7 @@ data NewUser = NewUser newtype B64UrlByteString = B64UrlByteString ByteString deriving (Eq, Show) - deriving newtype (FromField) + deriving newtype (FromField, Encoding) instance ToField B64UrlByteString where toField (B64UrlByteString m) = toField $ Binary m @@ -764,7 +764,8 @@ data GroupProfile = GroupProfile image :: Maybe ImageData, groupLink :: Maybe ShortLinkContact, groupPreferences :: Maybe GroupPreferences, - memberAdmission :: Maybe GroupMemberAdmission + memberAdmission :: Maybe GroupMemberAdmission, + sharedGroupId :: Maybe B64UrlByteString -- group identity = sha256(genesis root key), immutable } deriving (Eq, Show)