From 7d5768cf3a9ca6420ab7b8be91073018dd46a2f3 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Tue, 27 Jan 2026 17:56:31 +0000 Subject: [PATCH] core: prepare group link before creating the group (#6600) * core: prepare group link before creating the group * update group creation flow * refactor * comments * update plan, schema, api docs/types * store shared group ID and keys when joining relay groups * query plans, api docs --- bots/api/TYPES.md | 42 ++++++- bots/src/API/Docs/Types.hs | 16 ++- bots/src/API/TypeInfo.hs | 5 +- cabal.project | 2 +- docs/contributing/CODE.md | 5 + .../types/typescript/src/types.ts | 36 +++++- scripts/nix/sha256map.nix | 2 +- src/Simplex/Chat/Controller.hs | 13 +- src/Simplex/Chat/Library/Commands.hs | 113 +++++++++--------- src/Simplex/Chat/Library/Internal.hs | 4 +- src/Simplex/Chat/Library/Subscriber.hs | 2 +- src/Simplex/Chat/Store/Connections.hs | 5 +- src/Simplex/Chat/Store/Groups.hs | 93 +++++++++----- src/Simplex/Chat/Store/Messages.hs | 8 +- .../Migrations/M20260222_chat_relays.hs | 20 +++- .../Store/Postgres/Migrations/chat_schema.sql | 9 +- .../Migrations/M20260222_chat_relays.hs | 10 ++ .../SQLite/Migrations/chat_query_plans.txt | 75 +++++++----- .../Store/SQLite/Migrations/chat_schema.sql | 7 +- src/Simplex/Chat/Store/Shared.hs | 26 ++-- src/Simplex/Chat/Types.hs | 27 ++++- src/Simplex/Chat/View.hs | 7 +- 22 files changed, 371 insertions(+), 156 deletions(-) diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md index 02ec262986..25a6565f45 100644 --- a/bots/api/TYPES.md +++ b/bots/api/TYPES.md @@ -90,6 +90,7 @@ This file is generated automatically. - [GroupFeature](#groupfeature) - [GroupFeatureEnabled](#groupfeatureenabled) - [GroupInfo](#groupinfo) +- [GroupKeys](#groupkeys) - [GroupLink](#grouplink) - [GroupLinkPlan](#grouplinkplan) - [GroupMember](#groupmember) @@ -103,7 +104,9 @@ This file is generated automatically. - [GroupPreferences](#grouppreferences) - [GroupProfile](#groupprofile) - [GroupRelay](#grouprelay) +- [GroupRootKey](#grouprootkey) - [GroupShortLinkData](#groupshortlinkdata) +- [GroupShortLinkInfo](#groupshortlinkinfo) - [GroupSummary](#groupsummary) - [GroupSupportChat](#groupsupportchat) - [HandshakeError](#handshakeerror) @@ -2158,6 +2161,17 @@ MemberSupport: - groupSummary: [GroupSummary](#groupsummary) - membersRequireAttention: int - viaGroupLinkUri: string? +- groupKeys: [GroupKeys](#groupkeys)? + + +--- + +## GroupKeys + +**Record type**: +- sharedGroupId: string +- groupRootKey: [GroupRootKey](#grouprootkey) +- memberPrivKey: string --- @@ -2181,7 +2195,7 @@ MemberSupport: Ok: - type: "ok" -- direct: bool +- groupSLinkInfo_: [GroupShortLinkInfo](#groupshortlinkinfo)? - groupSLinkData_: [GroupShortLinkData](#groupshortlinkdata)? OwnLink: @@ -2225,6 +2239,7 @@ Known: - createdAt: UTCTime - updatedAt: UTCTime - supportChat: [GroupSupportChat](#groupsupportchat)? +- memberPubKey: string? --- @@ -2353,6 +2368,21 @@ Known: - relayLink: string? +--- + +## GroupRootKey + +**Discriminated union type**: + +Private: +- type: "private" +- rootPrivKey: string + +Public: +- type: "public" +- rootPubKey: string + + --- ## GroupShortLinkData @@ -2361,6 +2391,16 @@ Known: - groupProfile: [GroupProfile](#groupprofile) +--- + +## GroupShortLinkInfo + +**Record type**: +- direct: bool +- groupRelays: [string] +- sharedGroupId: string? + + --- ## GroupSummary diff --git a/bots/src/API/Docs/Types.hs b/bots/src/API/Docs/Types.hs index 77201172f5..10d8368857 100644 --- a/bots/src/API/Docs/Types.hs +++ b/bots/src/API/Docs/Types.hs @@ -256,7 +256,7 @@ chatTypesDocsData = (sti @FileError, STUnion, "FileErr", [], "", ""), (sti @FileErrorType, STUnion, "", [], "", ""), (sti @FileInvitation, STRecord, "", [], "", ""), - (sti @FileProtocol, (STEnum' $ consLower "FP"), "", [], "", ""), + (sti @FileProtocol, STEnum' (consLower "FP"), "", [], "", ""), (sti @FileStatus, STEnum, "FS", [], "", ""), (sti @FileTransferMeta, STRecord, "", [], "", ""), (sti @Format, STUnion, "", ["Unknown"], "", ""), @@ -269,20 +269,23 @@ chatTypesDocsData = (sti @GroupFeature, STEnum, "GF", [], "", ""), (sti @GroupFeatureEnabled, STEnum, "FE", [], "", ""), (sti @GroupInfo, STRecord, "", [], "", ""), + (sti @GroupKeys, STRecord, "", [], "", ""), + (sti @GroupRootKey, STUnion, "GRK", [], "", ""), (sti @GroupLink, STRecord, "", [], "", ""), (sti @GroupLinkPlan, STUnion, "GLP", [], "", ""), (sti @GroupMember, STRecord, "", [], "", ""), (sti @GroupMemberAdmission, STRecord, "", [], "", ""), - (sti @GroupMemberCategory, (STEnum' $ dropPfxSfx "GC" "Member"), "", [], "", ""), + (sti @GroupMemberCategory, STEnum' (dropPfxSfx "GC" "Member"), "", [], "", ""), (sti @GroupMemberRef, STRecord, "", [], "", ""), - (sti @GroupMemberRole, (STEnum' $ dropPfxSfx "GR" ""), "", ["GRUnknown"], "", ""), + (sti @GroupMemberRole, STEnum' (dropPfxSfx "GR" ""), "", ["GRUnknown"], "", ""), (sti @GroupMemberSettings, STRecord, "", [], "", ""), - (sti @GroupMemberStatus, (STEnum' $ (\case "group_deleted" -> "deleted"; "intro_invited" -> "intro-inv"; s -> s) . consSep "GSMem" '_'), "", [], "", ""), + (sti @GroupMemberStatus, STEnum' ((\case "group_deleted" -> "deleted"; "intro_invited" -> "intro-inv"; s -> s) . consSep "GSMem" '_'), "", [], "", ""), (sti @GroupPreference, STRecord, "", [], "", ""), (sti @GroupPreferences, STRecord, "", [], "", ""), (sti @GroupProfile, STRecord, "", [], "", ""), (sti @GroupRelay, STRecord, "", [], "", ""), (sti @GroupShortLinkData, STRecord, "", [], "", ""), + (sti @GroupShortLinkInfo, STRecord, "", [], "", ""), (sti @GroupSummary, STRecord, "", [], "", ""), (sti @GroupSupportChat, STRecord, "", [], "", ""), (sti @HandshakeError, STEnum, "", [], "", ""), @@ -322,7 +325,7 @@ chatTypesDocsData = (sti @RcvFileTransfer, STRecord, "", [], "", ""), (sti @RcvGroupEvent, STUnion, "RGE", [], "", ""), (sti @RelayStatus, STEnum, "RS", [], "", ""), - (sti @ReportReason, (STEnum' $ dropPfxSfx "RR" ""), "", ["RRUnknown"], "", ""), + (sti @ReportReason, STEnum' (dropPfxSfx "RR" ""), "", ["RRUnknown"], "", ""), (sti @RoleGroupPreference, STRecord, "", [], "", ""), (sti @SecurityCode, STRecord, "", [], "", ""), (sti @SimplePreference, STRecord, "", [], "", ""), @@ -458,6 +461,8 @@ deriving instance Generic GroupChatScopeInfo deriving instance Generic GroupFeature deriving instance Generic GroupFeatureEnabled deriving instance Generic GroupInfo +deriving instance Generic GroupKeys +deriving instance Generic GroupRootKey deriving instance Generic GroupLink deriving instance Generic GroupLinkPlan deriving instance Generic GroupMember @@ -472,6 +477,7 @@ deriving instance Generic GroupPreferences deriving instance Generic GroupProfile deriving instance Generic GroupRelay deriving instance Generic GroupShortLinkData +deriving instance Generic GroupShortLinkInfo deriving instance Generic GroupSummary deriving instance Generic GroupSupportChat deriving instance Generic HandshakeError diff --git a/bots/src/API/TypeInfo.hs b/bots/src/API/TypeInfo.hs index a70de72d01..61e2595fa9 100644 --- a/bots/src/API/TypeInfo.hs +++ b/bots/src/API/TypeInfo.hs @@ -1,5 +1,4 @@ {-# LANGUAGE AllowAmbiguousTypes #-} -{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} @@ -8,9 +7,7 @@ {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TypeOperators #-} -{-# LANGUAGE TypeSynonymInstances #-} {-# LANGUAGE TypeApplications #-} {-# OPTIONS_GHC -Wno-orphans #-} @@ -210,6 +207,8 @@ toTypeInfo tr = "MemberId", "Text", "MREmojiChar", + "PrivateKey", + "PublicKey", "ProtocolServer", "SbKey", "SharedMsgId", diff --git a/cabal.project b/cabal.project index a03d492bd6..e1c7ada5c7 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 89b81d151fa0378196d923c5d7fa0aea08462136 + tag: d10e05b7968e7f20313bf6f5b07f2290d9420e0d source-repository-package type: git diff --git a/docs/contributing/CODE.md b/docs/contributing/CODE.md index 6cdd7972f5..ab91b54d04 100644 --- a/docs/contributing/CODE.md +++ b/docs/contributing/CODE.md @@ -41,6 +41,11 @@ Some files that use CPP language extension cannot be formatted as a whole, so in - Never do refactoring unless it substantially reduces cost of solving the current problem, including the cost of refactoring - Aim to minimize the code changes - do what is minimally required to solve users' problems +**Code analysis and review:** +- Trace data flows end-to-end: from origin, through storage/parameters, to consumption. Flag values that are discarded and reconstructed from partial data (e.g. extracted from a URI missing original fields) — this is usually a bug. +- Read implementations of called functions, not just signatures — if duplication involves a called function, check whether decomposing it resolves the duplication. +- Do not save time on analysis. Read every function in the data flow even when the interface seems clear — wrong assumptions about internals are the main source of missed bugs. + ### Haskell Extensions - `StrictData` enabled by default - Use STM for safe concurrency diff --git a/packages/simplex-chat-client/types/typescript/src/types.ts b/packages/simplex-chat-client/types/typescript/src/types.ts index cce79229d0..c04ff3466e 100644 --- a/packages/simplex-chat-client/types/typescript/src/types.ts +++ b/packages/simplex-chat-client/types/typescript/src/types.ts @@ -2442,6 +2442,13 @@ export interface GroupInfo { groupSummary: GroupSummary membersRequireAttention: number // int viaGroupLinkUri?: string + groupKeys?: GroupKeys +} + +export interface GroupKeys { + sharedGroupId: string + groupRootKey: GroupRootKey + memberPrivKey: string } export interface GroupLink { @@ -2469,7 +2476,7 @@ export namespace GroupLinkPlan { export interface Ok extends Interface { type: "ok" - direct: boolean + groupSLinkInfo_?: GroupShortLinkInfo groupSLinkData_?: GroupShortLinkData } @@ -2514,6 +2521,7 @@ export interface GroupMember { createdAt: string // ISO-8601 timestamp updatedAt: string // ISO-8601 timestamp supportChat?: GroupSupportChat + memberPubKey?: string } export interface GroupMemberAdmission { @@ -2602,10 +2610,36 @@ export interface GroupRelay { relayLink?: string } +export type GroupRootKey = GroupRootKey.Private | GroupRootKey.Public + +export namespace GroupRootKey { + export type Tag = "private" | "public" + + interface Interface { + type: Tag + } + + export interface Private extends Interface { + type: "private" + rootPrivKey: string + } + + export interface Public extends Interface { + type: "public" + rootPubKey: string + } +} + export interface GroupShortLinkData { groupProfile: GroupProfile } +export interface GroupShortLinkInfo { + direct: boolean + groupRelays: string[] + sharedGroupId?: string +} + export interface GroupSummary { currentMembers: number // int64 } diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 8e3f039472..767334154d 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."89b81d151fa0378196d923c5d7fa0aea08462136" = "033vzd7f62plb9ncf8lbdn894682phxp53ysvry9ch79mlf68yqf"; + "https://github.com/simplex-chat/simplexmq.git"."d10e05b7968e7f20313bf6f5b07f2290d9420e0d" = "1gnsg5xv4m6w0pd34qpb9vg6b458rwyhkpzi3rqm0nxb9xjhpd61"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index de35e4d730..6fc6818730 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -994,7 +994,7 @@ data ContactAddressPlan deriving (Show) data GroupLinkPlan - = GLPOk {direct :: DirectLink, groupSLinkData_ :: Maybe GroupShortLinkData} + = GLPOk {groupSLinkInfo_ :: Maybe GroupShortLinkInfo, groupSLinkData_ :: Maybe GroupShortLinkData} | GLPOwnLink {groupInfo :: GroupInfo} | GLPConnectingConfirmReconnect | GLPConnectingProhibit {groupInfo_ :: Maybe GroupInfo} @@ -1003,6 +1003,13 @@ data GroupLinkPlan type DirectLink = Bool +data GroupShortLinkInfo = GroupShortLinkInfo + { direct :: Bool, + groupRelays :: [ShortLinkContact], + sharedGroupId :: Maybe B64UrlByteString + } + deriving (Show) + connectionPlanProceed :: ConnectionPlan -> Bool connectionPlanProceed = \case CPInvitationLink ilp -> case ilp of @@ -1016,7 +1023,7 @@ connectionPlanProceed = \case CAPContactViaAddress _ -> True _ -> False CPGroupLink glp -> case glp of - GLPOk _direct _ -> True + GLPOk {} -> True GLPOwnLink _ -> True GLPConnectingConfirmReconnect -> True _ -> False @@ -1598,6 +1605,8 @@ $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "ILP") ''InvitationLinkPlan) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CAP") ''ContactAddressPlan) +$(JQ.deriveJSON defaultJSON ''GroupShortLinkInfo) + $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "GLP") ''GroupLinkPlan) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "FC") ''ForwardConfirmation) diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 0b1cc979c0..3a7069e825 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -97,6 +97,7 @@ import qualified Simplex.Messaging.Agent.Store.DB as DB import Simplex.Messaging.Agent.Store.Interface (getCurrentMigrations) import Simplex.Messaging.Client (NetworkConfig (..), NetworkRequestMode (..), NetworkTimeout (..), SMPWebPortServers (..), SocksMode (SMAlways), textToHostMode) import qualified Simplex.Messaging.Crypto as C +import qualified Simplex.Messaging.Crypto.ShortLink as SL import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import qualified Simplex.Messaging.Crypto.File as CF import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..), pattern IKPQOff, pattern IKPQOn, pattern PQEncOff, pattern PQSupportOff, pattern PQSupportOn) @@ -1991,11 +1992,15 @@ processChatCommand vr nm = \case sLnk <- case toShortLinkContact connLinkToConnect of Just sl -> pure sl Nothing -> throwChatError $ CEException "failed to retrieve relays: no short link" - (mainCReq@(CRContactUri crData), ContactLinkData _ UserContactData {relays}) <- getShortLinkConnReq nm user sLnk + (FixedLinkData {linkConnReq = mainCReq@(CRContactUri crData), linkEntityId, rootKey}, ContactLinkData _ UserContactData {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} gInfo' <- withFastStore $ \db -> setPreparedGroupLinkInfo db vr user gInfo mainCReq cReqHash incognitoProfile + forM_ linkEntityId $ \sharedGroupId -> do + gVar <- asks random + (_, memberPrivKey) <- liftIO $ atomically $ C.generateKeyPair gVar + withFastStore' $ \db -> updateGroupMemberKeys db groupId sharedGroupId rootKey memberPrivKey (groupMemberId' $ membership gInfo') rs <- mapConcurrently (connectToRelay gInfo') relays let relayFailed = \case (_, _, Left _) -> True; _ -> False (failed, succeeded) = partition relayFailed rs @@ -2030,8 +2035,9 @@ processChatCommand vr nm = \case -- Save relayLink to re-use relay member record on retry (check by relayLink) relayMember <- withFastStore $ \db -> getCreateRelayForMember db vr gVar user gInfo' relayLink r <- tryAllErrors $ do - (cReq, _cData) <- getShortLinkConnReq nm user relayLink - let relayLinkToConnect = CCLink cReq (Just relayLink) + (fd, _cData) <- getShortLinkConnReq nm user relayLink + let cReq = linkConnReq fd + relayLinkToConnect = CCLink cReq (Just relayLink) void $ connectViaContact user (Just $ PCEGroup gInfo' relayMember) incognito relayLinkToConnect Nothing Nothing -- Re-read member to get updated activeConn relayMember' <- withFastStore $ \db -> getGroupMember db vr user groupId (groupMemberId' relayMember) @@ -2088,7 +2094,7 @@ processChatCommand vr nm = \case ccLink <- case contactLink of Just (CLFull cReq) -> pure $ CCLink cReq Nothing Just (CLShort sLnk) -> do - (cReq, _cData) <- getShortLinkConnReq nm user sLnk + (FixedLinkData {linkConnReq = cReq}, _cData) <- getShortLinkConnReq nm user sLnk pure $ CCLink cReq $ Just sLnk Nothing -> throwCmdError "no address in contact profile" connectContactViaAddress user incognito ct ccLink `catchAllErrors` \e -> do @@ -2311,50 +2317,48 @@ processChatCommand vr nm = \case chatItemId <- getChatItemIdByText user chatRef msg processChatCommand vr nm $ APIChatItemReaction chatRef chatItemId add reaction APINewGroup userId incognito gProfile -> withUserId userId $ \user -> do - gInfo <- newGroup user incognito gProfile False + g <- asks random + memberId <- liftIO $ MemberId <$> encodedRandomBytes g 12 + gInfo <- newGroup user incognito gProfile False memberId Nothing pure $ CRGroupCreated user gInfo NewGroup incognito gProfile -> withUser $ \User {userId} -> processChatCommand vr nm $ APINewGroup userId incognito gProfile - APINewPublicGroup userId incognito relayIds gProfile -> withUserId userId $ \user -> do - gInfo <- newGroup user incognito gProfile True - (gInfo', gLink, groupRelays) <- setupLink user gInfo `catchAllErrors` \e -> do + APINewPublicGroup userId incognito relayIds groupProfile -> withUserId userId $ \user -> do + (gProfile', memberId, groupKeys, setupLink) <- prepareGroupLink user + gInfo <- newGroup user incognito gProfile' True memberId (Just groupKeys) + (gLink, groupRelays) <- setupLink gInfo `catchAllErrors` \e -> do deleteInProgressGroup user gInfo throwError e - pure $ CRPublicGroupCreated user gInfo' gLink groupRelays + pure $ CRPublicGroupCreated user gInfo gLink groupRelays where - setupLink :: User -> GroupInfo -> CM (GroupInfo, GroupLink, [GroupRelay]) - setupLink user gInfo = do - (gInfo', gLink, sLnk) <- newGroupLink user gInfo - relays <- withFastStore $ \db -> mapM (getChatRelayById db user) (L.toList relayIds) - groupRelays <- addRelays user gInfo' sLnk relays - pure (gInfo', gLink, groupRelays) - newGroupLink :: User -> GroupInfo -> CM (GroupInfo, GroupLink, ShortLinkContact) - newGroupLink user gInfo@GroupInfo {groupProfile} = do + prepareGroupLink :: User -> CM (GroupProfile, MemberId, GroupKeys, GroupInfo -> CM (GroupLink, [GroupRelay])) + prepareGroupLink user = do + gVar <- asks random groupLinkId <- GroupLinkId <$> drgRandomBytes 16 + sharedGroupId <- drgRandomBytes 24 subMode <- chatReadVar subscriptionMode - -- TODO [relays] owner: prepare group link without initially creating on server - -- TODO - add link and owner key to group profile, sign profile - -- TODO - create group link on server with signed profile as data - -- / link creation - let userData = encodeShortLinkData $ GroupShortLinkData groupProfile - userLinkData = UserContactLinkData UserContactData {direct = False, owners = [], relays = [], userData} - crClientData = encodeJSON $ CRDataGroup groupLinkId - (connId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True True SCMContact (Just userLinkData) (Just crClientData) IKPQOff subMode + let 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' <- createdGroupLink <$> shortenCreatedLink ccLink sLnk <- case toShortLinkContact ccLink' of Just sl -> pure sl Nothing -> throwChatError $ CEException "failed to create relayed group link: no short link" + -- 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} - userData' = encodeShortLinkData $ GroupShortLinkData groupProfile' - userLinkData' = UserContactLinkData UserContactData {direct = False, owners = [], relays = [], userData = userData'} - void $ withAgent (\a -> setConnShortLink a nm connId SCMContact userLinkData' (Just crClientData)) - -- link creation / - gVar <- asks random - (gInfo', gLink) <- withFastStore $ \db -> do - gLink <- createGroupLink db gVar user gInfo connId ccLink' groupLinkId GRMember subMode - gInfo' <- updateGroupProfile db user gInfo groupProfile' - pure (gInfo', gLink) - pure (gInfo', gLink, sLnk) + userData = encodeShortLinkData $ GroupShortLinkData groupProfile' + userLinkData = UserContactLinkData UserContactData {direct = False, owners = [ownerAuth], relays = [], userData} + -- create connection with prepared link (single network call) + connId <- withAgent $ \a -> createConnectionForLink a nm (aUserId user) True ccLink preparedParams userLinkData IKPQOff subMode + let groupKeys = GroupKeys {sharedGroupId = B64UrlByteString sharedGroupId, groupRootKey = GRKPrivate rootPrivKey, memberPrivKey} + setupLink gInfo = do + gLink <- withFastStore $ \db -> createGroupLink db gVar user gInfo connId ccLink' groupLinkId GRMember subMode + relays <- withFastStore $ \db -> mapM (getChatRelayById db user) (L.toList relayIds) + groupRelays <- addRelays user gInfo sLnk relays + pure (gLink, groupRelays) + pure (groupProfile', memberId, groupKeys, setupLink) NewPublicGroup incognito relayIds gProfile -> withUser $ \User {userId} -> processChatCommand vr nm $ APINewPublicGroup userId incognito relayIds gProfile APIAddMember groupId contactId memRole -> withUser $ \user -> withGroupLock "addMember" groupId $ do @@ -3625,13 +3629,12 @@ processChatCommand vr nm = \case groupId <- getGroupIdByName db user gName groupMemberId <- getGroupMemberIdByName db user groupId groupMemberName pure (groupId, groupMemberId) - newGroup :: User -> IncognitoEnabled -> GroupProfile -> Bool -> CM GroupInfo - newGroup user incognito gProfile@GroupProfile {displayName} useRelays = do + newGroup :: User -> IncognitoEnabled -> GroupProfile -> Bool -> MemberId -> Maybe GroupKeys -> CM GroupInfo + newGroup user incognito gProfile@GroupProfile {displayName} useRelays memberId groupKeys_ = do checkValidName displayName - gVar <- asks random -- [incognito] generate incognito profile for group membership incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing - gInfo <- withFastStore $ \db -> createNewGroup db vr gVar user gProfile incognitoProfile useRelays + gInfo <- withFastStore $ \db -> createNewGroup db vr user gProfile incognitoProfile useRelays memberId groupKeys_ let cd = CDGroupSnd gInfo Nothing createInternalChatItem user cd CIChatBanner (Just epochStart) createInternalChatItem user cd (CISndGroupE2EEInfo E2EInfo {pqEnabled = Just PQEncOff}) Nothing @@ -3667,7 +3670,7 @@ processChatCommand vr nm = \case -- TODO [relays] owner: track and reuse relay profiles -- TODO - single profile linked to relay configuration record (chat_relays) -- TODO - update when fetching link data from relay address - (cReq, _cData) <- getShortLinkConnReq nm user address + (FixedLinkData {linkConnReq = cReq}, _cData) <- getShortLinkConnReq nm user address lift (withAgent' $ \a -> connRequestPQSupport a PQSupportOff cReq) >>= \case Nothing -> throwChatError CEInvalidConnReq Just (agentV, _) -> do @@ -3767,7 +3770,7 @@ processChatCommand vr nm = \case knownLinkPlans l' >>= \case Just r -> pure r Nothing -> do - (cReq, cData) <- getShortLinkConnReq nm user l' + (FixedLinkData {linkConnReq = cReq}, cData) <- getShortLinkConnReq nm user l' contactSLinkData_ <- liftIO $ decodeLinkUserData cData invitationReqAndPlan cReq (Just l') contactSLinkData_ where @@ -3793,7 +3796,7 @@ processChatCommand vr nm = \case knownLinkPlans >>= \case Just r -> pure r Nothing -> do - (cReq, cData) <- getShortLinkConnReq nm user l' + (FixedLinkData {linkConnReq = cReq}, cData) <- getShortLinkConnReq nm user l' withFastStore' (\db -> getContactWithoutConnViaShortAddress db vr user l') >>= \case Just ct' | not (contactDeleted ct') -> pure (con cReq, CPContactAddress (CAPContactViaAddress ct')) _ -> do @@ -3812,9 +3815,11 @@ processChatCommand vr nm = \case knownLinkPlans >>= \case Just r -> pure r Nothing -> do - (cReq, cData@(ContactLinkData _ UserContactData {direct})) <- getShortLinkConnReq nm user l' + (fd, cData@(ContactLinkData _ UserContactData {direct, relays})) <- getShortLinkConnReq nm user l' + let FixedLinkData {linkConnReq = cReq, linkEntityId} = fd + linkInfo = GroupShortLinkInfo {direct, groupRelays = relays, sharedGroupId = B64UrlByteString <$> linkEntityId} groupSLinkData_ <- liftIO $ decodeLinkUserData cData - plan <- groupJoinRequestPlan user cReq direct groupSLinkData_ + plan <- groupJoinRequestPlan user cReq (Just linkInfo) groupSLinkData_ pure (con cReq, plan) where knownLinkPlans = withFastStore $ \db -> @@ -3859,7 +3864,7 @@ processChatCommand vr nm = \case groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli case groupLinkId of Nothing -> contactRequestPlan user cReq Nothing - Just _ -> groupJoinRequestPlan user cReq True Nothing + Just _ -> groupJoinRequestPlan user cReq Nothing Nothing contactRequestPlan :: User -> ConnReqContact -> Maybe ContactShortLinkData -> CM ConnectionPlan contactRequestPlan user (CRContactUri crData) contactSLinkData_ = do let cReqSchemas = contactCReqSchemas crData @@ -3880,10 +3885,10 @@ processChatCommand vr nm = \case | contactDeleted ct -> pure $ CPContactAddress (CAPOk contactSLinkData_) | otherwise -> pure $ CPContactAddress (CAPKnown ct) -- TODO [short links] RcvGroupMsgConnection branch is deprecated? (old group link protocol?) - Just (RcvGroupMsgConnection _ gInfo _) -> groupPlan gInfo True Nothing + Just (RcvGroupMsgConnection _ gInfo _) -> groupPlan gInfo Nothing Nothing Just _ -> throwCmdError "found connection entity is not RcvDirectMsgConnection or RcvGroupMsgConnection" - groupJoinRequestPlan :: User -> ConnReqContact -> Bool -> Maybe GroupShortLinkData -> CM ConnectionPlan - groupJoinRequestPlan user (CRContactUri crData) direct groupSLinkData_ = do + groupJoinRequestPlan :: User -> ConnReqContact -> Maybe GroupShortLinkInfo -> Maybe GroupShortLinkData -> CM ConnectionPlan + groupJoinRequestPlan user (CRContactUri crData) groupSLinkInfo_ groupSLinkData_ = do let cReqSchemas = contactCReqSchemas crData cReqHashes = bimap contactCReqHash contactCReqHash cReqSchemas withFastStore' (\db -> getGroupInfoByUserContactLinkConnReq db vr user cReqSchemas) >>= \case @@ -3892,21 +3897,21 @@ processChatCommand vr nm = \case connEnt_ <- withFastStore' $ \db -> getContactConnEntityByConnReqHash db vr user cReqHashes gInfo_ <- withFastStore' $ \db -> getGroupInfoByGroupLinkHash db vr user cReqHashes case (gInfo_, connEnt_) of - (Nothing, Nothing) -> pure $ CPGroupLink (GLPOk direct groupSLinkData_) + (Nothing, Nothing) -> pure $ CPGroupLink (GLPOk groupSLinkInfo_ groupSLinkData_) -- TODO [short links] RcvDirectMsgConnection branches are deprecated? (old group link protocol?) (Nothing, Just (RcvDirectMsgConnection _conn Nothing)) -> pure $ CPGroupLink GLPConnectingConfirmReconnect (Nothing, Just (RcvDirectMsgConnection _ (Just ct))) | not (contactReady ct) && contactActive ct -> pure $ CPGroupLink (GLPConnectingProhibit gInfo_) - | otherwise -> pure $ CPGroupLink (GLPOk direct groupSLinkData_) + | otherwise -> pure $ CPGroupLink (GLPOk groupSLinkInfo_ groupSLinkData_) (Nothing, Just _) -> throwCmdError "found connection entity is not RcvDirectMsgConnection" - (Just gInfo, _) -> groupPlan gInfo direct groupSLinkData_ - groupPlan :: GroupInfo -> Bool -> Maybe GroupShortLinkData -> CM ConnectionPlan - groupPlan gInfo@GroupInfo {membership} direct groupSLinkData_ + (Just gInfo, _) -> groupPlan gInfo groupSLinkInfo_ groupSLinkData_ + groupPlan :: GroupInfo -> Maybe GroupShortLinkInfo -> Maybe GroupShortLinkData -> CM ConnectionPlan + groupPlan gInfo@GroupInfo {membership} groupSLinkInfo_ groupSLinkData_ | memberStatus membership == GSMemRejected = pure $ CPGroupLink (GLPKnown gInfo) | not (memberActive membership) && not (memberRemoved membership) = pure $ CPGroupLink (GLPConnectingProhibit $ Just gInfo) | memberActive membership = pure $ CPGroupLink (GLPKnown gInfo) - | otherwise = pure $ CPGroupLink (GLPOk direct groupSLinkData_) + | otherwise = pure $ CPGroupLink (GLPOk groupSLinkInfo_ groupSLinkData_) contactCReqSchemas :: ConnReqUriData -> (ConnReqContact, ConnReqContact) contactCReqSchemas crData = ( CRContactUri crData {crScheme = SSSimplex}, diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index 2a3d71ea73..7ea5150583 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -1308,7 +1308,7 @@ groupLinkData gInfo@GroupInfo {groupProfile} GroupLink {groupLinkId} groupRelays restoreShortLink' :: ConnShortLink m -> CM (ConnShortLink m) restoreShortLink' l = (`restoreShortLink` l) <$> asks (shortLinkPresetServers . config) -getShortLinkConnReq :: NetworkRequestMode -> User -> ConnShortLink m -> CM (ConnectionRequestUri m, ConnLinkData m) +getShortLinkConnReq :: NetworkRequestMode -> User -> ConnShortLink m -> CM (FixedLinkData m, ConnLinkData m) getShortLinkConnReq nm user@User {userChatRelay} l = do l' <- restoreShortLink' l (fd, cData) <- withAgent $ \a -> getConnShortLink a nm (aUserId user) l' @@ -1318,7 +1318,7 @@ getShortLinkConnReq nm user@User {userChatRelay} l = do where supported = direct || not (null relays) || isTrue userChatRelay _ -> pure () - pure (linkConnReq fd, cData) + pure (fd, cData) encodeShortLinkData :: J.ToJSON a => a -> UserLinkData encodeShortLinkData d = diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index a5e5c0fcd7..1f1b56598c 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -3438,7 +3438,7 @@ runRelayRequestWorker a Worker {doWork} = do where getLinkDataCreateRelayLink :: RelayRequestData -> GroupInfo -> CM (GroupInfo, ShortLinkContact) getLinkDataCreateRelayLink RelayRequestData {reqGroupLink} gInfo = do - (_cReq, cData) <- getShortLinkConnReq NRMBackground user reqGroupLink + (_fd, cData) <- getShortLinkConnReq NRMBackground user reqGroupLink liftIO (decodeLinkUserData cData) >>= \case Nothing -> throwChatError $ CEException "getLinkDataCreateRelayLink: no group link data" Just (GroupShortLinkData gp) -> do diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs index 9a007d85fe..0e689267d4 100644 --- a/src/Simplex/Chat/Store/Connections.hs +++ b/src/Simplex/Chat/Store/Connections.hs @@ -142,18 +142,19 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do g.business_chat, g.business_member_id, g.customer_member_id, g.use_relays, g.relay_own_status, g.ui_themes, g.summary_current_members_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, + g.shared_group_id, g.root_priv_key, g.root_pub_key, g.member_priv_key, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, -- GroupInfo {membership = GroupMember {memberProfile}} pu.display_name, pu.full_name, pu.short_descr, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, mu.created_at, mu.updated_at, - mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, + mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, -- from GroupMember m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, m.created_at, m.updated_at, - m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) JOIN groups g ON g.group_id = m.group_id diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 8564354a97..7abc865ef1 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -101,6 +101,7 @@ module Simplex.Chat.Store.Groups getMemberInvitation, createMemberConnection, createMemberConnectionAsync, + updateGroupMemberKeys, updateGroupMemberStatus, updateGroupMemberStatusById, updateGroupMemberAccepted, @@ -208,11 +209,11 @@ import Database.SQLite.Simple (Only (..), Query, (:.) (..)) import Database.SQLite.Simple.QQ (sql) #endif -type MaybeGroupMemberRow = (Maybe GroupMemberId, Maybe GroupId, Maybe Int64, Maybe MemberId, Maybe VersionChat, Maybe VersionChat, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId) :. (Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, Maybe LocalAlias, Maybe Preferences) :. (Maybe UTCTime, Maybe UTCTime) :. (Maybe UTCTime, Maybe Int64, Maybe Int64, Maybe Int64, Maybe UTCTime) +type MaybeGroupMemberRow = (Maybe GroupMemberId, Maybe GroupId, Maybe Int64, Maybe MemberId, Maybe VersionChat, Maybe VersionChat, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId) :. (Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, Maybe LocalAlias, Maybe Preferences) :. (Maybe UTCTime, Maybe UTCTime) :. (Maybe UTCTime, Maybe Int64, Maybe Int64, Maybe Int64, Maybe UTCTime, Maybe C.PublicKeyEd25519) toMaybeGroupMember :: Int64 -> MaybeGroupMemberRow -> Maybe GroupMember -toMaybeGroupMember userContactId ((Just groupMemberId, Just groupId, Just indexInGroup, Just memberId, Just minVer, Just maxVer, Just memberRole, Just memberCategory, Just memberStatus, Just showMessages, memberBlocked') :. (invitedById, invitedByGroupMemberId, Just localDisplayName, memberContactId, Just memberContactProfileId) :. (Just profileId, Just displayName, Just fullName, shortDescr, image, contactLink, peerType, Just localAlias, contactPreferences) :. (Just createdAt, Just updatedAt) :. (supportChatTs, Just supportChatUnread, Just supportChatUnanswered, Just supportChatMentions, supportChatLastMsgFromMemberTs)) = - Just $ toGroupMember userContactId ((groupMemberId, groupId, indexInGroup, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages, memberBlocked') :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId) :. (profileId, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias, contactPreferences) :. (createdAt, updatedAt) :. (supportChatTs, supportChatUnread, supportChatUnanswered, supportChatMentions, supportChatLastMsgFromMemberTs)) +toMaybeGroupMember userContactId ((Just groupMemberId, Just groupId, Just indexInGroup, Just memberId, Just minVer, Just maxVer, Just memberRole, Just memberCategory, Just memberStatus, Just showMessages, memberBlocked') :. (invitedById, invitedByGroupMemberId, Just localDisplayName, memberContactId, Just memberContactProfileId) :. (Just profileId, Just displayName, Just fullName, shortDescr, image, contactLink, peerType, Just localAlias, contactPreferences) :. (Just createdAt, Just updatedAt) :. (supportChatTs, Just supportChatUnread, Just supportChatUnanswered, Just supportChatMentions, supportChatLastMsgFromMemberTs, memberPubKey)) = + Just $ toGroupMember userContactId ((groupMemberId, groupId, indexInGroup, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages, memberBlocked') :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId) :. (profileId, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias, contactPreferences) :. (createdAt, updatedAt) :. (supportChatTs, supportChatUnread, supportChatUnanswered, supportChatMentions, supportChatLastMsgFromMemberTs, memberPubKey)) toMaybeGroupMember _ _ = Nothing createGroupLink :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> ConnId -> CreatedLinkContact -> GroupLinkId -> GroupMemberRole -> SubscriptionMode -> ExceptT StoreError IO GroupLink @@ -327,13 +328,20 @@ setGroupLinkShortLink db gLnk@GroupLink {userContactLinkId, connLinkContact = CC pure gLnk {connLinkContact = CCLink connFullLink (Just shortLink), shortLinkDataSet = True, shortLinkLargeDataSet = BoolDef True} -- | creates completely new group with a single member - the current user -createNewGroup :: DB.Connection -> VersionRangeChat -> TVar ChaChaDRG -> User -> GroupProfile -> Maybe Profile -> Bool -> ExceptT StoreError IO GroupInfo -createNewGroup db vr gVar user@User {userId} groupProfile incognitoProfile useRelays = ExceptT $ do +createNewGroup :: DB.Connection -> VersionRangeChat -> User -> GroupProfile -> Maybe Profile -> Bool -> MemberId -> Maybe GroupKeys -> ExceptT StoreError IO GroupInfo +createNewGroup db vr user@User {userId} groupProfile incognitoProfile useRelays memberId groupKeys = ExceptT $ do let GroupProfile {displayName, fullName, shortDescr, description, image, groupPreferences, memberAdmission} = groupProfile fullGroupPreferences = mergeGroupPreferences groupPreferences currentTs <- getCurrentTime customUserProfileId <- mapM (createIncognitoProfile_ db userId currentTs) incognitoProfile withLocalDisplayName db userId displayName $ \ldn -> runExceptT $ do + let (sharedGroupId_, rootPrivKey_, rootPubKey_, memberPrivKey_) = case groupKeys of + Nothing -> (Nothing, Nothing, Nothing, Nothing) + Just GroupKeys {sharedGroupId, groupRootKey, memberPrivKey} -> + let (rpk, rpub) = case groupRootKey of + GRKPrivate pk -> (Just pk, Nothing) + GRKPublic k -> (Nothing, Just k) + in (Just sharedGroupId, rpk, rpub, Just memberPrivKey) groupId <- liftIO $ do DB.execute db @@ -345,13 +353,16 @@ createNewGroup db vr gVar user@User {userId} groupProfile incognitoProfile useRe [sql| 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) - VALUES (?,?,?,?,?,?,?,?,?,?) + created_at, updated_at, chat_ts, user_member_profile_sent_at, + shared_group_id, root_priv_key, root_pub_key, member_priv_key) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] - (BI useRelays, BI useRelays, ldn, userId, profileId, BI True, currentTs, currentTs, currentTs, currentTs) + ( (BI useRelays, BI useRelays, ldn, userId, profileId, BI True, currentTs, currentTs, currentTs, currentTs) + :. (sharedGroupId_, rootPrivKey_, rootPubKey_, memberPrivKey_) + ) insertedRowId db - memberId <- liftIO $ encodedRandomBytes gVar 12 - membership <- createContactMemberInv_ db user groupId Nothing user (MemberIdRole (MemberId memberId) GROwner) GCUserMember GSMemCreator IBUser customUserProfileId currentTs vr + let memberPubKey = C.publicKey . memberPrivKey <$> groupKeys + membership <- createContactMemberInv_ db user groupId Nothing user (MemberIdRole memberId GROwner) GCUserMember GSMemCreator IBUser customUserProfileId memberPubKey currentTs vr let chatSettings = ChatSettings {enableNtfs = MFAll, sendRcpts = Nothing, favorite = False} pure GroupInfo @@ -376,7 +387,8 @@ createNewGroup db vr gVar user@User {userId} groupProfile incognitoProfile useRe groupSummary = GroupSummary 1, customData = Nothing, membersRequireAttention = 0, - viaGroupLinkUri = Nothing + viaGroupLinkUri = Nothing, + groupKeys } -- | creates a new group record for the group the current user was invited to, or returns an existing one @@ -426,8 +438,10 @@ createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activ ((profileId, localDisplayName, connRequest, userId, BI True, currentTs, currentTs, currentTs, currentTs) :. businessChatInfoRow business) insertedRowId db let hostVRange = adjustedMemberVRange vr peerChatVRange - GroupMember {groupMemberId} <- createContactMemberInv_ db user groupId Nothing contact fromMember GCHostMember GSMemInvited IBUnknown Nothing currentTs hostVRange - membership <- createContactMemberInv_ db user groupId (Just groupMemberId) user invitedMember GCUserMember GSMemInvited (IBContact contactId) incognitoProfileId currentTs vr + -- TODO [member keys] inviting host should generate its keys in public groups + GroupMember {groupMemberId} <- createContactMemberInv_ db user groupId Nothing contact fromMember GCHostMember GSMemInvited IBUnknown Nothing Nothing currentTs hostVRange + -- TODO [member keys] relay should pass key received via XMember + membership <- createContactMemberInv_ db user groupId (Just groupMemberId) user invitedMember GCUserMember GSMemInvited (IBContact contactId) incognitoProfileId Nothing currentTs vr let chatSettings = ChatSettings {enableNtfs = MFAll, sendRcpts = Nothing, favorite = False} pure ( GroupInfo @@ -452,7 +466,8 @@ createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activ groupSummary = GroupSummary 2, customData = Nothing, membersRequireAttention = 0, - viaGroupLinkUri = Nothing + viaGroupLinkUri = Nothing, + groupKeys = Nothing }, groupMemberId ) @@ -485,8 +500,8 @@ getUpdateNextIndexInGroup_ db groupId = |] (Only groupId) -createContactMemberInv_ :: IsContact a => DB.Connection -> User -> GroupId -> Maybe GroupMemberId -> a -> MemberIdRole -> GroupMemberCategory -> GroupMemberStatus -> InvitedBy -> Maybe ProfileId -> UTCTime -> VersionRangeChat -> ExceptT StoreError IO GroupMember -createContactMemberInv_ db User {userId, userContactId} groupId invitedByGroupMemberId userOrContact MemberIdRole {memberId, memberRole} memberCategory memberStatus invitedBy incognitoProfileId createdAt vr = do +createContactMemberInv_ :: IsContact a => DB.Connection -> User -> GroupId -> Maybe GroupMemberId -> a -> MemberIdRole -> GroupMemberCategory -> GroupMemberStatus -> InvitedBy -> Maybe ProfileId -> Maybe C.PublicKeyEd25519 -> UTCTime -> VersionRangeChat -> ExceptT StoreError IO GroupMember +createContactMemberInv_ db User {userId, userContactId} groupId invitedByGroupMemberId userOrContact MemberIdRole {memberId, memberRole} memberCategory memberStatus invitedBy incognitoProfileId memberPubKey createdAt vr = do incognitoProfile <- forM incognitoProfileId $ \profileId -> getProfileById db userId profileId (indexInGroup, localDisplayName, memberProfile) <- case (incognitoProfile, incognitoProfileId) of (Just profile@LocalProfile {displayName}, Just profileId) -> do @@ -517,7 +532,8 @@ createContactMemberInv_ db User {userId, userContactId} groupId invitedByGroupMe memberChatVRange, createdAt, updatedAt = createdAt, - supportChat = Nothing + supportChat = Nothing, + memberPubKey } where memberChatVRange@(VersionRange minV maxV) = vr @@ -531,12 +547,12 @@ createContactMemberInv_ db User {userId, userContactId} groupId invitedByGroupMe [sql| INSERT INTO group_members ( group_id, index_in_group, member_id, member_role, member_category, member_status, member_relations_vector, 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, fromInvitedBy userContactId invitedBy, invitedByGroupMemberId) - :. (userId, localDisplayName' userOrContact, contactId' userOrContact, localProfileId $ profile' userOrContact, createdAt, createdAt) + :. (userId, localDisplayName' userOrContact, contactId' userOrContact, localProfileId $ profile' userOrContact, memberPubKey, createdAt, createdAt) :. (minV, maxV) ) pure (indexInGroup, localDisplayName) @@ -550,12 +566,12 @@ createContactMemberInv_ db User {userId, userContactId} groupId invitedByGroupMe [sql| INSERT INTO group_members ( group_id, index_in_group, member_id, member_role, member_category, member_status, member_relations_vector, invited_by, invited_by_group_member_id, - user_id, local_display_name, contact_id, contact_profile_id, member_profile_id, created_at, updated_at, + user_id, local_display_name, contact_id, contact_profile_id, member_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, fromInvitedBy userContactId invitedBy, invitedByGroupMemberId) - :. (userId, incognitoLdn, contactId' userOrContact, localProfileId $ profile' userOrContact, customUserProfileId, createdAt, createdAt) + :. (userId, incognitoLdn, contactId' userOrContact, localProfileId $ profile' userOrContact, customUserProfileId, memberPubKey, createdAt, createdAt) :. (minV, maxV) ) pure (indexInGroup, incognitoLdn) @@ -577,7 +593,8 @@ createPreparedGroup db gVar vr user@User {userId, userContactId} groupProfile bu then liftIO $ MemberId <$> encodedRandomBytes gVar 12 else pure $ MemberId $ encodeUtf8 groupLDN <> "_user_unknown_id" let userMember = MemberIdRole userMemberId GRMember - membership <- createContactMemberInv_ db user groupId (Just hostMemberId) user userMember GCUserMember GSMemUnknown IBUnknown Nothing currentTs vr + -- TODO [member keys] user key must be included here. Should key be added when group is prepared? + membership <- createContactMemberInv_ db user groupId (Just hostMemberId) user userMember GCUserMember GSMemUnknown IBUnknown Nothing Nothing currentTs vr hostMember <- getGroupMember db vr user groupId hostMemberId when business $ liftIO $ setGroupBusinessChatInfo groupId membership hostMember g <- getGroupInfo db vr user groupId @@ -777,7 +794,8 @@ createGroupViaLink' hostMemberId <- insertHost_ currentTs groupId liftIO $ DB.execute db "UPDATE connections SET conn_type = ?, group_member_id = ?, updated_at = ? WHERE connection_id = ?" (ConnMember, hostMemberId, currentTs, connId) -- using IBUnknown since host is created without contact - void $ createContactMemberInv_ db user groupId (Just hostMemberId) user invitedMember GCUserMember membershipStatus IBUnknown customUserProfileId currentTs vr + -- TODO [member keys] can this be used with public groups? if yes member keys need to be added + void $ createContactMemberInv_ db user groupId (Just hostMemberId) user invitedMember GCUserMember membershipStatus IBUnknown customUserProfileId Nothing currentTs vr liftIO $ setViaGroupLinkUri db groupId connId (,) <$> getGroupInfo db vr user groupId <*> getGroupMemberById db vr user hostMemberId where @@ -1207,7 +1225,8 @@ createNewContactMember db gVar User {userId, userContactId} GroupInfo {groupId, memberChatVRange = peerChatVRange, createdAt, updatedAt = createdAt, - supportChat = Nothing + supportChat = Nothing, + memberPubKey = Nothing } where insertMember_ = do @@ -1417,7 +1436,8 @@ createRelayRequestGroup db vr user@User {userId} GroupRelayInvitation {fromMembe liftIO $ setRelayRequestData_ groupId ownerMemberId <- insertOwner_ currentTs groupId let relayMember = MemberIdRole relayMemberId GRRelay - _membership <- createContactMemberInv_ db user groupId (Just ownerMemberId) user relayMember GCUserMember GSMemAccepted IBUnknown Nothing currentTs vr + -- TODO [member keys] should relays use member keys? + _membership <- createContactMemberInv_ db user groupId (Just ownerMemberId) user relayMember GCUserMember GSMemAccepted IBUnknown Nothing Nothing currentTs vr ownerMember <- getGroupMember db vr user groupId ownerMemberId g <- getGroupInfo db vr user groupId pure (g, ownerMember) @@ -1603,7 +1623,8 @@ createBusinessRequestGroup (groupProfileId, ldn, userId, BI True, currentTs, currentTs, currentTs, currentTs, BCCustomer) groupId <- liftIO $ insertedRowId db memberId <- liftIO $ encodedRandomBytes gVar 12 - membership <- createContactMemberInv_ db user groupId Nothing user (MemberIdRole (MemberId memberId) GROwner) GCUserMember GSMemCreator IBUser Nothing currentTs vr + -- TODO [member keys] we could support member keys in business groups to allow binding agreements (though identity keys would be better for it. + membership <- createContactMemberInv_ db user groupId Nothing user (MemberIdRole (MemberId memberId) GROwner) GCUserMember GSMemCreator IBUser Nothing Nothing currentTs vr pure (groupId, membership) VersionRange minV maxV = cReqChatVRange insertClientMember_ currentTs groupId membership = @@ -1665,6 +1686,18 @@ createMemberConnectionAsync db user@User {userId} groupMemberId (cmdId, agentCon Connection {connId} <- createMemberConnection_ db userId groupMemberId agentConnId chatV peerChatVRange Nothing 0 currentTs subMode setCommandConnId db user cmdId connId +updateGroupMemberKeys :: DB.Connection -> GroupId -> ByteString -> C.PublicKeyEd25519 -> C.PrivateKeyEd25519 -> GroupMemberId -> IO () +updateGroupMemberKeys db groupId sharedGroupId rootPubKey memberPrivKey membershipGMId = do + currentTs <- getCurrentTime + DB.execute + db + "UPDATE groups SET shared_group_id = ?, root_pub_key = ?, member_priv_key = ?, updated_at = ? WHERE group_id = ?" + (sharedGroupId, rootPubKey, memberPrivKey, currentTs, groupId) + DB.execute + db + "UPDATE group_members SET member_pub_key = ?, updated_at = ? WHERE group_member_id = ?" + (C.publicKey memberPrivKey, currentTs, membershipGMId) + updateGroupMemberStatus :: DB.Connection -> UserId -> GroupMember -> GroupMemberStatus -> IO () updateGroupMemberStatus db userId GroupMember {groupMemberId} = updateGroupMemberStatusById db userId groupMemberId @@ -1849,7 +1882,9 @@ createNewMember_ memberChatVRange, createdAt, updatedAt = createdAt, - supportChat = Nothing + supportChat = Nothing, + -- TODO [member keys] is it used with relay/public groups? + memberPubKey = Nothing } checkGroupMemberHasItems :: DB.Connection -> User -> GroupMember -> IO (Maybe ChatItemId) diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index 7093c89eec..15b17cd19c 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -679,7 +679,7 @@ getChatItemQuote_ db User {userId, userContactId} chatDirection QuotedMsg {msgRe m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, m.created_at, m.updated_at, - m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN contacts c ON m.contact_id = c.contact_id @@ -2977,7 +2977,7 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, m.created_at, m.updated_at, - m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, -- quoted ChatItem ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent, -- quoted GroupMember @@ -2985,13 +2985,13 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do rm.member_status, rm.show_messages, rm.member_restriction, rm.invited_by, rm.invited_by_group_member_id, rm.local_display_name, rm.contact_id, rm.contact_profile_id, rp.contact_profile_id, rp.display_name, rp.full_name, rp.short_descr, rp.image, rp.contact_link, rp.chat_peer_type, rp.local_alias, rp.preferences, rm.created_at, rm.updated_at, - rm.support_chat_ts, rm.support_chat_items_unread, rm.support_chat_items_member_attention, rm.support_chat_items_mentions, rm.support_chat_last_msg_from_member_ts, + rm.support_chat_ts, rm.support_chat_items_unread, rm.support_chat_items_member_attention, rm.support_chat_items_mentions, rm.support_chat_last_msg_from_member_ts, rm.member_pub_key, -- deleted by GroupMember dbm.group_member_id, dbm.group_id, dbm.index_in_group, dbm.member_id, dbm.peer_chat_min_version, dbm.peer_chat_max_version, dbm.member_role, dbm.member_category, dbm.member_status, dbm.show_messages, dbm.member_restriction, dbm.invited_by, dbm.invited_by_group_member_id, dbm.local_display_name, dbm.contact_id, dbm.contact_profile_id, dbp.contact_profile_id, dbp.display_name, dbp.full_name, dbp.short_descr, dbp.image, dbp.contact_link, dbp.chat_peer_type, dbp.local_alias, dbp.preferences, dbm.created_at, dbm.updated_at, - dbm.support_chat_ts, dbm.support_chat_items_unread, dbm.support_chat_items_member_attention, dbm.support_chat_items_mentions, dbm.support_chat_last_msg_from_member_ts + dbm.support_chat_ts, dbm.support_chat_items_unread, dbm.support_chat_items_member_attention, dbm.support_chat_items_mentions, dbm.support_chat_last_msg_from_member_ts, dbm.member_pub_key FROM chat_items i LEFT JOIN files f ON f.chat_item_id = i.chat_item_id LEFT JOIN group_members m ON m.group_member_id = i.group_member_id diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20260222_chat_relays.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20260222_chat_relays.hs index f86ee049e4..905067bf76 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations/M20260222_chat_relays.hs +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20260222_chat_relays.hs @@ -38,7 +38,11 @@ ALTER TABLE groups ADD COLUMN relay_request_peer_chat_min_version INTEGER, ADD COLUMN relay_request_peer_chat_max_version INTEGER, ADD COLUMN relay_request_failed SMALLINT DEFAULT 0, - ADD COLUMN relay_request_err_reason TEXT; + ADD COLUMN relay_request_err_reason TEXT, + ADD COLUMN shared_group_id BYTEA, + ADD COLUMN root_priv_key BYTEA, + ADD COLUMN root_pub_key BYTEA, + ADD COLUMN member_priv_key BYTEA; ALTER TABLE group_profiles ADD COLUMN group_link BYTEA; @@ -56,7 +60,9 @@ CREATE INDEX idx_group_relays_group_id ON group_relays(group_id); CREATE UNIQUE INDEX idx_group_relays_group_member_id ON group_relays(group_member_id); CREATE INDEX idx_group_relays_chat_relay_id ON group_relays(chat_relay_id); -ALTER TABLE group_members ADD COLUMN relay_link BYTEA; +ALTER TABLE group_members + ADD COLUMN relay_link BYTEA, + ADD COLUMN member_pub_key BYTEA; |] down_m20260222_chat_relays :: Text @@ -74,7 +80,11 @@ ALTER TABLE groups DROP COLUMN relay_request_peer_chat_min_version, DROP COLUMN relay_request_peer_chat_max_version, DROP COLUMN relay_request_failed, - DROP COLUMN relay_request_err_reason; + DROP COLUMN relay_request_err_reason, + DROP COLUMN shared_group_id, + DROP COLUMN root_priv_key, + DROP COLUMN root_pub_key, + DROP COLUMN member_priv_key; ALTER TABLE group_profiles DROP COLUMN group_link; @@ -88,5 +98,7 @@ DROP INDEX idx_chat_relays_user_id_address; DROP INDEX idx_chat_relays_user_id_name; DROP TABLE chat_relays; -ALTER TABLE group_members DROP COLUMN relay_link; +ALTER TABLE group_members + DROP COLUMN relay_link, + DROP COLUMN member_pub_key; |] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql index bcdcd5c992..3f617ab780 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql @@ -811,7 +811,8 @@ CREATE TABLE test_chat_schema.group_members ( member_welcome_shared_msg_id bytea, index_in_group bigint DEFAULT 0 NOT NULL, member_relations_vector bytea, - relay_link bytea + relay_link bytea, + member_pub_key bytea ); @@ -945,7 +946,11 @@ CREATE TABLE test_chat_schema.groups ( relay_request_peer_chat_min_version integer, relay_request_peer_chat_max_version integer, relay_request_failed smallint DEFAULT 0, - relay_request_err_reason text + relay_request_err_reason text, + shared_group_id bytea, + root_priv_key bytea, + root_pub_key bytea, + member_priv_key bytea ); diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20260222_chat_relays.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20260222_chat_relays.hs index eb2fb9f258..594ea37035 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/M20260222_chat_relays.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20260222_chat_relays.hs @@ -52,6 +52,10 @@ ALTER TABLE groups ADD COLUMN relay_request_peer_chat_min_version INTEGER; ALTER TABLE groups ADD COLUMN relay_request_peer_chat_max_version INTEGER; ALTER TABLE groups ADD COLUMN relay_request_failed INTEGER DEFAULT 0; ALTER TABLE groups ADD COLUMN relay_request_err_reason TEXT; +ALTER TABLE groups ADD COLUMN shared_group_id BLOB; +ALTER TABLE groups ADD COLUMN root_priv_key BLOB; +ALTER TABLE groups ADD COLUMN root_pub_key BLOB; +ALTER TABLE groups ADD COLUMN member_priv_key BLOB; ALTER TABLE group_profiles ADD COLUMN group_link BLOB; @@ -70,6 +74,7 @@ CREATE UNIQUE INDEX idx_group_relays_group_member_id ON group_relays(group_membe CREATE INDEX idx_group_relays_chat_relay_id ON group_relays(chat_relay_id); ALTER TABLE group_members ADD COLUMN relay_link BLOB; +ALTER TABLE group_members ADD COLUMN member_pub_key BLOB; |] down_m20260222_chat_relays :: Query @@ -89,6 +94,10 @@ ALTER TABLE groups DROP COLUMN relay_request_peer_chat_min_version; ALTER TABLE groups DROP COLUMN relay_request_peer_chat_max_version; ALTER TABLE groups DROP COLUMN relay_request_failed; ALTER TABLE groups DROP COLUMN relay_request_err_reason; +ALTER TABLE groups DROP COLUMN shared_group_id; +ALTER TABLE groups DROP COLUMN root_priv_key; +ALTER TABLE groups DROP COLUMN root_pub_key; +ALTER TABLE groups DROP COLUMN member_priv_key; ALTER TABLE group_profiles DROP COLUMN group_link; @@ -103,4 +112,5 @@ DROP INDEX idx_chat_relays_user_id_name; DROP TABLE chat_relays; ALTER TABLE group_members DROP COLUMN relay_link; +ALTER TABLE group_members DROP COLUMN member_pub_key; |] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt index 566fe89768..a61e6a34e0 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -149,18 +149,19 @@ Query: g.business_chat, g.business_member_id, g.customer_member_id, g.use_relays, g.relay_own_status, g.ui_themes, g.summary_current_members_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, + g.shared_group_id, g.root_priv_key, g.root_pub_key, g.member_priv_key, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, -- GroupInfo {membership = GroupMember {memberProfile}} pu.display_name, pu.full_name, pu.short_descr, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, mu.created_at, mu.updated_at, - mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, + mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, -- from GroupMember m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, m.created_at, m.updated_at, - m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) JOIN groups g ON g.group_id = m.group_id @@ -313,9 +314,9 @@ SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (conta Query: INSERT INTO group_members ( group_id, index_in_group, member_id, member_role, member_category, member_status, member_relations_vector, invited_by, invited_by_group_member_id, - user_id, local_display_name, contact_id, contact_profile_id, member_profile_id, created_at, updated_at, + user_id, local_display_name, contact_id, contact_profile_id, member_profile_id, member_pub_key, created_at, updated_at, peer_chat_min_version, peer_chat_max_version) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) @@ -617,9 +618,9 @@ SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (conta Query: INSERT INTO group_members ( group_id, index_in_group, member_id, member_role, member_category, member_status, member_relations_vector, 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 (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) @@ -1009,7 +1010,7 @@ Query: m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, m.created_at, m.updated_at, - m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN contacts c ON m.contact_id = c.contact_id @@ -1214,8 +1215,9 @@ Plan: Query: 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) - VALUES (?,?,?,?,?,?,?,?,?,?) + created_at, updated_at, chat_ts, user_member_profile_sent_at, + shared_group_id, root_priv_key, root_pub_key, member_priv_key) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: @@ -1274,7 +1276,7 @@ Query: m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, m.created_at, m.updated_at, - m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, -- quoted ChatItem ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent, -- quoted GroupMember @@ -1282,13 +1284,13 @@ Query: rm.member_status, rm.show_messages, rm.member_restriction, rm.invited_by, rm.invited_by_group_member_id, rm.local_display_name, rm.contact_id, rm.contact_profile_id, rp.contact_profile_id, rp.display_name, rp.full_name, rp.short_descr, rp.image, rp.contact_link, rp.chat_peer_type, rp.local_alias, rp.preferences, rm.created_at, rm.updated_at, - rm.support_chat_ts, rm.support_chat_items_unread, rm.support_chat_items_member_attention, rm.support_chat_items_mentions, rm.support_chat_last_msg_from_member_ts, + rm.support_chat_ts, rm.support_chat_items_unread, rm.support_chat_items_member_attention, rm.support_chat_items_mentions, rm.support_chat_last_msg_from_member_ts, rm.member_pub_key, -- deleted by GroupMember dbm.group_member_id, dbm.group_id, dbm.index_in_group, dbm.member_id, dbm.peer_chat_min_version, dbm.peer_chat_max_version, dbm.member_role, dbm.member_category, dbm.member_status, dbm.show_messages, dbm.member_restriction, dbm.invited_by, dbm.invited_by_group_member_id, dbm.local_display_name, dbm.contact_id, dbm.contact_profile_id, dbp.contact_profile_id, dbp.display_name, dbp.full_name, dbp.short_descr, dbp.image, dbp.contact_link, dbp.chat_peer_type, dbp.local_alias, dbp.preferences, dbm.created_at, dbm.updated_at, - dbm.support_chat_ts, dbm.support_chat_items_unread, dbm.support_chat_items_member_attention, dbm.support_chat_items_mentions, dbm.support_chat_last_msg_from_member_ts + dbm.support_chat_ts, dbm.support_chat_items_unread, dbm.support_chat_items_member_attention, dbm.support_chat_items_mentions, dbm.support_chat_last_msg_from_member_ts, dbm.member_pub_key FROM chat_items i LEFT JOIN files f ON f.chat_item_id = i.chat_item_id LEFT JOIN group_members m ON m.group_member_id = i.group_member_id @@ -5103,12 +5105,13 @@ Query: g.business_chat, g.business_member_id, g.customer_member_id, g.use_relays, g.relay_own_status, g.ui_themes, g.summary_current_members_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, + g.shared_group_id, g.root_priv_key, g.root_pub_key, g.member_priv_key, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.short_descr, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, mu.created_at, mu.updated_at, - mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts + mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key FROM groups g JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id @@ -5138,12 +5141,13 @@ Query: g.business_chat, g.business_member_id, g.customer_member_id, g.use_relays, g.relay_own_status, g.ui_themes, g.summary_current_members_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, + g.shared_group_id, g.root_priv_key, g.root_pub_key, g.member_priv_key, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.short_descr, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, mu.created_at, mu.updated_at, - mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts + mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key FROM groups g JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id @@ -5166,12 +5170,13 @@ Query: g.business_chat, g.business_member_id, g.customer_member_id, g.use_relays, g.relay_own_status, g.ui_themes, g.summary_current_members_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, + g.shared_group_id, g.root_priv_key, g.root_pub_key, g.member_priv_key, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.short_descr, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, mu.created_at, mu.updated_at, - mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts + mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key FROM groups g JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id @@ -5219,7 +5224,7 @@ Query: m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, m.created_at, m.updated_at, - m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, @@ -5246,7 +5251,7 @@ Query: m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, m.created_at, m.updated_at, - m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, @@ -5265,7 +5270,7 @@ Query: m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, m.created_at, m.updated_at, - m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, @@ -5284,7 +5289,7 @@ Query: m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, m.created_at, m.updated_at, - m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, @@ -5303,7 +5308,7 @@ Query: m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, m.created_at, m.updated_at, - m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, @@ -5322,7 +5327,7 @@ Query: m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, m.created_at, m.updated_at, - m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, @@ -5341,7 +5346,7 @@ Query: m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, m.created_at, m.updated_at, - m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, @@ -5360,7 +5365,7 @@ Query: m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, m.created_at, m.updated_at, - m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, @@ -5379,7 +5384,7 @@ Query: m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, m.created_at, m.updated_at, - m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, @@ -5398,7 +5403,7 @@ Query: m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, m.created_at, m.updated_at, - m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, @@ -5417,7 +5422,7 @@ Query: m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, m.created_at, m.updated_at, - m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, @@ -6395,7 +6400,7 @@ SEARCH contacts USING INDEX idx_contacts_chat_ts (user_id=?) Query: SELECT COUNT(1) FROM groups WHERE user_id = ? AND chat_item_ttl > 0 Plan: -SEARCH groups USING INDEX idx_groups_chat_ts (user_id=?) +SEARCH groups USING INDEX sqlite_autoindex_groups_2 (user_id=?) Query: SELECT COUNT(1), COALESCE(SUM(user_mention), 0) FROM chat_items WHERE user_id = ? AND group_id = ? AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL AND item_status = ? Plan: @@ -6557,7 +6562,7 @@ SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT group_id FROM groups WHERE inv_queue_info = ? AND user_id = ? LIMIT 1 Plan: -SEARCH groups USING INDEX idx_groups_chat_ts (user_id=?) +SEARCH groups USING INDEX sqlite_autoindex_groups_2 (user_id=?) Query: SELECT group_id FROM groups WHERE user_id = ? AND chat_item_ttl > 0 OR chat_item_ttl IS NULL Plan: @@ -6565,7 +6570,7 @@ SCAN groups Query: SELECT group_id FROM groups WHERE user_id = ? AND creating_in_progress = 1 AND created_at <= ? Plan: -SEARCH groups USING INDEX idx_groups_chat_ts (user_id=?) +SEARCH groups USING INDEX sqlite_autoindex_groups_2 (user_id=?) Query: SELECT group_id FROM groups WHERE user_id = ? AND local_display_name = ? Plan: @@ -6577,7 +6582,7 @@ SEARCH user_contact_links USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT group_id, conn_full_link_to_connect FROM groups WHERE user_id = ? AND conn_short_link_to_connect = ? Plan: -SEARCH groups USING INDEX idx_groups_chat_ts (user_id=?) +SEARCH groups USING INDEX sqlite_autoindex_groups_2 (user_id=?) Query: SELECT group_member_id FROM group_members WHERE user_id = ? AND contact_profile_id = ? AND group_member_id != ? LIMIT 1 Plan: @@ -6851,6 +6856,10 @@ Query: UPDATE group_members SET member_profile_id = ?, updated_at = ? WHERE grou Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) +Query: UPDATE group_members SET member_pub_key = ?, updated_at = ? WHERE group_member_id = ? +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE group_members SET member_relations_vector = set_member_vector_new_relation(member_relations_vector, ?, ?, ?), updated_at = ? WHERE group_member_id = ? Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) @@ -6931,6 +6940,10 @@ Query: UPDATE groups SET send_rcpts = NULL Plan: SCAN groups +Query: UPDATE groups SET shared_group_id = ?, root_pub_key = ?, member_priv_key = ?, updated_at = ? WHERE group_id = ? +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE groups SET ui_themes = ?, updated_at = ? WHERE user_id = ? AND group_id = ? Plan: SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) @@ -6941,7 +6954,7 @@ SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) Query: UPDATE groups SET unread_chat = ?, updated_at = ? WHERE user_id = ? AND unread_chat = ? Plan: -SEARCH groups USING INDEX idx_groups_chat_ts (user_id=?) +SEARCH groups USING INDEX sqlite_autoindex_groups_2 (user_id=?) Query: UPDATE groups SET user_member_profile_sent_at = ? WHERE user_id = ? AND group_id = ? Plan: diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql index 4a0348db4b..12951307bb 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql @@ -167,7 +167,11 @@ CREATE TABLE groups( relay_request_peer_chat_min_version INTEGER, relay_request_peer_chat_max_version INTEGER, relay_request_failed INTEGER DEFAULT 0, - relay_request_err_reason TEXT, -- received + relay_request_err_reason TEXT, + shared_group_id BLOB, + root_priv_key BLOB, + root_pub_key BLOB, + member_priv_key BLOB, -- received FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE CASCADE @@ -210,6 +214,7 @@ CREATE TABLE group_members( index_in_group INTEGER NOT NULL DEFAULT 0, member_relations_vector BLOB, relay_link BLOB, + member_pub_key BLOB, FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE CASCADE diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index 15c2c4d41c..90ccd4dedc 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -16,6 +16,7 @@ module Simplex.Chat.Store.Shared where +import Control.Applicative ((<|>)) import Control.Exception (Exception) import qualified Control.Exception as E import Control.Monad @@ -662,14 +663,16 @@ type PreparedGroupRow = (Maybe ConnReqContact, Maybe ShortLinkContact, BoolInt, type BusinessChatInfoRow = (Maybe BusinessChatType, Maybe MemberId, Maybe MemberId) -type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Text, Maybe Text, Maybe ImageData, Maybe ShortLinkContact) :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe GroupPreferences, Maybe GroupMemberAdmission) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. PreparedGroupRow :. BusinessChatInfoRow :. (BoolInt, Maybe RelayStatus, Maybe UIThemeEntityOverrides, Int64, Maybe CustomData, Maybe Int64, Int, Maybe ConnReqContact) :. GroupMemberRow +type GroupKeysRow = (Maybe B64UrlByteString, Maybe C.PrivateKeyEd25519, Maybe C.PublicKeyEd25519, Maybe C.PrivateKeyEd25519) -type GroupMemberRow = (GroupMemberId, GroupId, Int64, MemberId, VersionChat, VersionChat, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId) :. ProfileRow :. (UTCTime, UTCTime) :. (Maybe UTCTime, Int64, Int64, Int64, Maybe UTCTime) +type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Text, Maybe Text, Maybe ImageData, Maybe ShortLinkContact) :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe GroupPreferences, Maybe GroupMemberAdmission) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. PreparedGroupRow :. BusinessChatInfoRow :. (BoolInt, Maybe RelayStatus, Maybe UIThemeEntityOverrides, Int64, Maybe CustomData, Maybe Int64, Int, Maybe ConnReqContact) :. GroupKeysRow :. GroupMemberRow + +type GroupMemberRow = (GroupMemberId, GroupId, Int64, MemberId, VersionChat, VersionChat, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId) :. ProfileRow :. (UTCTime, UTCTime) :. (Maybe UTCTime, Int64, Int64, Int64, Maybe UTCTime, Maybe C.PublicKeyEd25519) type ProfileRow = (ProfileId, ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias, Maybe Preferences) toGroupInfo :: VersionRangeChat -> Int64 -> [ChatTagId] -> GroupInfoRow -> GroupInfo -toGroupInfo vr userContactId chatTags ((groupId, localDisplayName, displayName, fullName, shortDescr, localAlias, description, image, groupLink) :. (enableNtfs_, sendRcpts, BI favorite, groupPreferences, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. preparedGroupRow :. businessRow :. (BI useRelays, relayOwnStatus, uiThemes, currentMembers, customData, chatItemTTL, membersRequireAttention, viaGroupLinkUri) :. userMemberRow) = +toGroupInfo vr userContactId chatTags ((groupId, localDisplayName, displayName, fullName, shortDescr, localAlias, description, image, groupLink) :. (enableNtfs_, sendRcpts, BI favorite, groupPreferences, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. preparedGroupRow :. businessRow :. (BI useRelays, relayOwnStatus, uiThemes, currentMembers, customData, chatItemTTL, membersRequireAttention, viaGroupLinkUri) :. groupKeysRow :. userMemberRow) = let membership = (toGroupMember userContactId userMemberRow) {memberChatVRange = vr} chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite} fullGroupPreferences = mergeGroupPreferences groupPreferences @@ -677,7 +680,8 @@ toGroupInfo vr userContactId chatTags ((groupId, localDisplayName, displayName, businessChat = toBusinessChatInfo businessRow preparedGroup = toPreparedGroup preparedGroupRow groupSummary = GroupSummary {currentMembers} - 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 = 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 toPreparedGroup = \case @@ -685,8 +689,15 @@ toPreparedGroup = \case Just PreparedGroup {connLinkToConnect = CCLink fullLink shortLink_, connLinkPreparedConnection, connLinkStartedConnection, welcomeSharedMsgId, requestSharedMsgId} _ -> Nothing +toGroupKeys :: GroupKeysRow -> Maybe GroupKeys +toGroupKeys = \case + (Just sharedGroupId, rootPrivKey_, rootPubKey_, Just memberPrivKey) -> + (\grk -> GroupKeys {sharedGroupId, groupRootKey = grk, memberPrivKey}) + <$> (GRKPrivate <$> rootPrivKey_ <|> GRKPublic <$> rootPubKey_) + _ -> Nothing + toGroupMember :: Int64 -> GroupMemberRow -> GroupMember -toGroupMember userContactId ((groupMemberId, groupId, indexInGroup, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, BI showMessages, memberRestriction_) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId) :. profileRow :. (createdAt, updatedAt) :. (supportChatTs_, supportChatUnread, supportChatMemberAttention, supportChatMentions, supportChatLastMsgFromMemberTs)) = +toGroupMember userContactId ((groupMemberId, groupId, indexInGroup, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, BI showMessages, memberRestriction_) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId) :. profileRow :. (createdAt, updatedAt) :. (supportChatTs_, supportChatUnread, supportChatMemberAttention, supportChatMentions, supportChatLastMsgFromMemberTs, memberPubKey)) = let memberProfile = rowToLocalProfile profileRow memberSettings = GroupMemberSettings {showMessages} blockedByAdmin = maybe False mrsBlocked memberRestriction_ @@ -713,7 +724,7 @@ groupMemberQuery = m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, m.created_at, m.updated_at, - m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, @@ -750,12 +761,13 @@ groupInfoQueryFields = g.business_chat, g.business_member_id, g.customer_member_id, g.use_relays, g.relay_own_status, g.ui_themes, g.summary_current_members_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, + g.shared_group_id, g.root_priv_key, g.root_pub_key, g.member_priv_key, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.short_descr, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, mu.created_at, mu.updated_at, - mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts + mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key |] groupInfoQueryFrom :: Query diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 28896329f9..02916d0d82 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -54,6 +54,7 @@ import Simplex.FileTransfer.Description (FileDigest) import Simplex.FileTransfer.Types (RcvFileId, SndFileId) import Simplex.Messaging.Agent.Protocol (ACorrId, ACreatedConnLink, AEventTag (..), AEvtTag (..), ConnId, ConnShortLink, ConnectionLink, ConnectionMode (..), ConnectionRequestUri, CreatedConnLink, InvitationId, SAEntity (..), UserId) import Simplex.Messaging.Agent.Store.DB (Binary (..), blobFieldDecoder, fromTextField_) +import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFileArgs (..)) import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport, pattern PQEncOff) import Simplex.Messaging.Encoding.String @@ -447,6 +448,22 @@ data Group = Group {groupInfo :: GroupInfo, members :: [GroupMember]} type GroupId = Int64 +data GroupRootKey + = GRKPrivate {rootPrivKey :: C.PrivateKeyEd25519} + | GRKPublic {rootPubKey :: C.PublicKeyEd25519} + deriving (Eq, Show) + +groupRootPubKey :: GroupRootKey -> C.PublicKeyEd25519 +groupRootPubKey (GRKPrivate pk) = C.publicKey pk +groupRootPubKey (GRKPublic pk) = pk + +data GroupKeys = GroupKeys + { sharedGroupId :: B64UrlByteString, + groupRootKey :: GroupRootKey, + memberPrivKey :: C.PrivateKeyEd25519 + } + deriving (Eq, Show) + data GroupInfo = GroupInfo { groupId :: GroupId, useRelays :: BoolDef, @@ -469,7 +486,8 @@ data GroupInfo = GroupInfo customData :: Maybe CustomData, groupSummary :: GroupSummary, membersRequireAttention :: Int, - viaGroupLinkUri :: Maybe ConnReqContact + viaGroupLinkUri :: Maybe ConnReqContact, + groupKeys :: Maybe GroupKeys } deriving (Eq, Show) @@ -963,7 +981,8 @@ data GroupMember = GroupMember memberChatVRange :: VersionRangeChat, createdAt :: UTCTime, updatedAt :: UTCTime, - supportChat :: Maybe GroupSupportChat + supportChat :: Maybe GroupSupportChat, + memberPubKey :: Maybe C.PublicKeyEd25519 } deriving (Eq, Show) @@ -2039,6 +2058,10 @@ instance FromJSON GroupSummary where parseJSON = $(JQ.mkParseJSON defaultJSON ''GroupSummary) omittedField = Just GroupSummary {currentMembers = 0} +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "GRK") ''GroupRootKey) + +$(JQ.deriveJSON defaultJSON ''GroupKeys) + $(JQ.deriveJSON defaultJSON ''GroupInfo) $(JQ.deriveJSON defaultJSON ''Group) diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 4192bf5f7f..b1fa59f9ef 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -2025,9 +2025,10 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case | business -> ("business address: " <>) _ -> ("contact address: " <>) CPGroupLink glp -> case glp of - GLPOk direct groupSLinkData -> - [grpLink $ if direct then "ok to connect directly" else "ok to connect via relays"] - <> [viewJSON groupSLinkData] -- | testView] -- TODO [relays] disable link data output in cli (uncomment testView) + GLPOk groupSLinkInfo_ groupSLinkData -> + let direct = maybe True (\(GroupShortLinkInfo {direct = d}) -> d) groupSLinkInfo_ + in [grpLink $ if direct then "ok to connect directly" else "ok to connect via relays"] + <> [viewJSON groupSLinkData] -- | testView] -- TODO [relays] disable link data output in cli (uncomment testView) GLPOwnLink g -> [grpLink "own link for group " <> ttyGroup' g] GLPConnectingConfirmReconnect -> [grpLink "connecting, allowed to reconnect"] GLPConnectingProhibit Nothing -> [grpLink "connecting"]