diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md index c1d2cbb4ba..200bc2d769 100644 --- a/bots/api/TYPES.md +++ b/bots/api/TYPES.md @@ -2385,6 +2385,7 @@ Known: - groupLink: string? - groupPreferences: [GroupPreferences](#grouppreferences)? - memberAdmission: [GroupMemberAdmission](#groupmemberadmission)? +- sharedGroupId: string? --- diff --git a/cabal.project b/cabal.project index 5b75f49edb..f742308908 100644 --- a/cabal.project +++ b/cabal.project @@ -21,7 +21,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 782cacfb3cc57883465eecc0b9b30662daf2b81f + tag: a1b762992b10aa3db1cd949e6c408c0043ace674 source-repository-package type: git diff --git a/docs/rfcs/2026-03-28-group-identity-binding.md b/docs/rfcs/2026-03-28-group-identity-binding.md new file mode 100644 index 0000000000..afc4ed965c --- /dev/null +++ b/docs/rfcs/2026-03-28-group-identity-binding.md @@ -0,0 +1,68 @@ +# Group identity and signature binding + +## Problem + +Group message signatures bind to a group identity via a prefix: + +``` +signedBytes = smpEncode (CBGroup, groupIdentity, memberId) <> messageBody +``` + +Using `groupRootKey` as identity is unstable: the root key is derived from the link's key pair, so link rotation (relay replacement, key compromise recovery) changes it, breaking existing bindings. + +Using an arbitrary entity ID is stable but not self-authenticating: any owner could copy another group's ID. + +## Design + +Use the **hash of the genesis root key** as group identity: + +``` +groupEntityId = sha256(genesisRootPubKey) +``` + +- Set at creation, never changes. +- Self-authenticating: derived from a key pair only the creator held. +- Stored as `linkEntityId` in the short link, and in the group profile distributed to all members. +- Used in the signature binding prefix instead of root key. + +### Why no validation now + +Current clients do not validate that `linkEntityId == sha256(rootKey)` on join. This is unconventional — normally, an unvalidated binding is pointless. Here it is deliberate forward-compatible design, not deferred work: + +- **Forward compatibility for joiners**: future link rotation will cause `rootKey` and `linkEntityId` to diverge. Current clients don't know how to verify a rotation chain, so they must accept diverged values. If we validated now, current clients could not join future rotated groups. Mobile clients have slow upgrade cycles and we have no mechanism to force upgrades, so we aim for at least 2-3 months backward compatibility for new features (1 year for existing). Validating now would force a breaking change on rotation. + +- **Forward compatibility for groups**: all groups created now have the correct binding (`entityId = sha256(rootKey)`). When a future protocol version introduces rotation and enforces validation, these groups are already compliant. Deferring the entity ID until then would mean some groups have IDs and some don't — a backward-compatibility problem. + +The cloning risk (copied entity ID in a malicious group) is acceptable now: groups are small, invite links come from trusted sources, and history merging on re-join is itself a future feature. By the time channels are large enough for cloning to matter, validation will be enforced. + +### Key hierarchy context + +The root key is a **bootstrap key**: it signs `OwnerAuth` entries to certify owners (see [simplexmq owner chain](https://github.com/simplex-chat/simplexmq/blob/master/rfcs/2025-04-04-short-links-for-groups.md#multiple-owners-managing-queue-data)), then need not be used again. Owner keys sign admin messages, group updates, and future rotation statements. This conceals the creator's identity — all owners are indistinguishable. + +Using the genesis root key *hash* as identity aligns with this: after rotation, the root key changes but the identity persists, bridged by owner-signed rotation statements. + +### What IS validated now + +- **Link vs profile consistency**: joiners validate that `linkEntityId` from the link matches `sharedGroupId` in the group profile. This prevents a directory or listing from substituting a different link for a group — the link is bound to the profile. This check remains valid after rotation (both preserve the original entity ID). + +- **Profile update immutability**: `sharedGroupId` in the group profile must not change. Clients reject `XGrpInfo` updates that modify it. + +### What is NOT validated now + +- **`linkEntityId == sha256(rootKey)`**: not checked on join. See "Why no validation now" above. + +## Changes + +### Done + +1. **Agent API** (`simplexmq`): `prepareConnectionLink` takes caller-provided root key pair and entity ID instead of generating the key internally. Caller controls both. + +2. **Link creation** (`Commands.hs`): owner generates root key pair, computes `sharedGroupId = sha256(rootPubKey)`, passes both to `prepareConnectionLink`. The entity ID is baked into signed `FixedLinkData`. + +### Remaining + +3. **Group profile**: add `sharedGroupId` field to `GroupProfile`, set from `linkEntityId` at genesis, immutable. Reject `XGrpInfo` updates that change it. + +4. **Joiner validation**: confirm `linkEntityId` from link matches `sharedGroupId` from group profile. + +5. **Signature binding**: change prefix from `smpEncode (CBGroup, groupRootPubKey, memberId)` to `smpEncode (CBGroup, sharedGroupId, memberId)` in both `groupMsgSigning` (signing) and `withVerifiedMsg` (verification). diff --git a/packages/simplex-chat-client/types/typescript/src/types.ts b/packages/simplex-chat-client/types/typescript/src/types.ts index e55904644c..c693fe1888 100644 --- a/packages/simplex-chat-client/types/typescript/src/types.ts +++ b/packages/simplex-chat-client/types/typescript/src/types.ts @@ -2674,6 +2674,7 @@ export interface GroupProfile { groupLink?: string groupPreferences?: GroupPreferences memberAdmission?: GroupMemberAdmission + sharedGroupId?: string } export interface GroupRelay { diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 27a906fb52..ecbcddfa7f 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."782cacfb3cc57883465eecc0b9b30662daf2b81f" = "0ck5hcj2yn540l11bbhn0ghgk49mfyqy0c4xqkbw1kk0fd9hhxs6"; + "https://github.com/simplex-chat/simplexmq.git"."a1b762992b10aa3db1cd949e6c408c0043ace674" = "0k9sw6sf8hlgdbxfjd3rgzgf5yzqlkfpqj7mh934myp2vn9xhvnb"; "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/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index c9d85b9f45..9de7d44a2b 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -2024,6 +2024,9 @@ processChatCommand vr nm = \case Nothing -> throwChatError $ CEException "failed to retrieve relays: no short link" (FixedLinkData {linkConnReq = mainCReq@(CRContactUri crData), linkEntityId, rootKey}, cData@(ContactLinkData _ UserContactData {owners, relays})) <- getShortLinkConnReq nm user sLnk groupSLinkData_ <- liftIO $ decodeLinkUserData cData + -- Validate link entity ID matches group profile's sharedGroupId (relay groups must have both) + forM_ groupSLinkData_ $ \GroupShortLinkData {groupProfile = GroupProfile {sharedGroupId}} -> + unless ((B64UrlByteString <$> linkEntityId) == sharedGroupId) $ throwChatError CEInvalidConnReq let publicGroupData_ = groupSLinkData_ >>= \GroupShortLinkData {publicGroupData} -> publicGroupData publicMemberCount_ = (\PublicGroupData {publicMemberCount} -> publicMemberCount) <$> publicGroupData_ -- Prepare group record once before connecting to relays (updatePreparedRelayedGroup): @@ -2382,11 +2385,13 @@ processChatCommand vr nm = \case prepareGroupLink user = do gVar <- asks random groupLinkId <- GroupLinkId <$> drgRandomBytes 16 - sharedGroupId <- drgRandomBytes 24 subMode <- chatReadVar subscriptionMode - let crClientData = encodeJSON $ CRDataGroup groupLinkId - -- prepare link with sharedGroupId as linkEntityId (no server request) - ((_, rootPrivKey), ccLink, preparedParams) <- withAgent $ \a -> prepareConnectionLink a (aUserId user) (Just sharedGroupId) True (Just crClientData) + -- generate root key pair; entity ID = sha256(rootPubKey) — see docs/rfcs/2026-03-28-group-identity-binding.md + rootKey@(rootPubKey, rootPrivKey) <- liftIO $ atomically $ C.generateKeyPair gVar + let entityId = C.sha256Hash $ C.pubKeyBytes rootPubKey + crClientData = encodeJSON $ CRDataGroup groupLinkId + -- prepare link with entityId as linkEntityId (no server request) + (ccLink, preparedParams) <- withAgent $ \a -> prepareConnectionLink a (aUserId user) rootKey entityId True (Just crClientData) ccLink' <- createdChannelLink <$> shortenCreatedLink ccLink sLnk <- case toShortLinkContact ccLink' of Just sl -> pure sl @@ -2394,12 +2399,12 @@ processChatCommand vr nm = \case -- generate owner key, OwnerAuth signed by root key memberId <- MemberId <$> liftIO (encodedRandomBytes gVar 12) (memberPrivKey, ownerAuth) <- liftIO $ SL.newOwnerAuth gVar (unMemberId memberId) rootPrivKey - let groupProfile' = (groupProfile :: GroupProfile) {groupLink = Just sLnk} + let groupProfile' = (groupProfile :: GroupProfile) {groupLink = Just sLnk, sharedGroupId = Just $ B64UrlByteString entityId} userData = encodeShortLinkData $ GroupShortLinkData {groupProfile = groupProfile', publicGroupData = Just (PublicGroupData 1)} userLinkData = UserContactLinkData UserContactData {direct = False, owners = [ownerAuth], relays = [], userData} -- create connection with prepared link (single network call) connId <- withAgent $ \a -> createConnectionForLink a nm (aUserId user) True ccLink preparedParams userLinkData IKPQOff subMode - let groupKeys = GroupKeys {sharedGroupId = B64UrlByteString sharedGroupId, groupRootKey = GRKPrivate rootPrivKey, memberPrivKey} + let groupKeys = GroupKeys {sharedGroupId = B64UrlByteString entityId, groupRootKey = GRKPrivate rootPrivKey, memberPrivKey} setupLink gInfo = do -- TODO [relays] starting role should be communicated in protocol from owner to relays subRole <- asks $ channelSubscriberRole . config @@ -3894,6 +3899,10 @@ processChatCommand vr nm = \case let FixedLinkData {linkConnReq = cReq, linkEntityId} = fd linkInfo = GroupShortLinkInfo {direct, groupRelays = relays, sharedGroupId = B64UrlByteString <$> linkEntityId} groupSLinkData_ <- liftIO $ decodeLinkUserData cData + -- Validate link entity ID matches group profile's sharedGroupId + forM_ groupSLinkData_ $ \GroupShortLinkData {groupProfile = GroupProfile {sharedGroupId}} -> + unless ((B64UrlByteString <$> linkEntityId) == sharedGroupId) $ + throwChatError CEInvalidConnReq plan <- groupJoinRequestPlan user cReq (Just linkInfo) groupSLinkData_ pure (con cReq, plan) where @@ -5086,7 +5095,7 @@ chatCommandP = { directMessages = Just DirectMessagesGroupPreference {enable = FEOn, role = Nothing}, history = Just HistoryGroupPreference {enable = FEOn} } - pure GroupProfile {displayName = gName, fullName = "", shortDescr, description = Nothing, image = Nothing, groupLink = Nothing, groupPreferences, memberAdmission = Nothing} + pure GroupProfile {displayName = gName, fullName = "", shortDescr, description = Nothing, image = Nothing, groupLink = Nothing, groupPreferences, memberAdmission = Nothing, sharedGroupId = Nothing} memberCriteriaP = ("all" $> Just MCAll) <|> ("off" $> Nothing) shortDescrP = do descr <- A.takeWhile1 isSpace *> (T.dropWhileEnd isSpace <$> textP) <|> pure "" diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index de8511bce9..20475ca606 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -1055,7 +1055,7 @@ acceptRelayJoinRequestAsync businessGroupProfile :: Profile -> GroupPreferences -> GroupProfile businessGroupProfile Profile {displayName, fullName, shortDescr, image} groupPreferences = - GroupProfile {displayName, fullName, description = Nothing, shortDescr, image, groupLink = Nothing, groupPreferences = Just groupPreferences, memberAdmission = Nothing} + GroupProfile {displayName, fullName, description = Nothing, shortDescr, image, groupLink = Nothing, groupPreferences = Just groupPreferences, memberAdmission = Nothing, sharedGroupId = Nothing} introduceToModerators :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> CM () introduceToModerators vr user gInfo@GroupInfo {groupId} m@GroupMember {memberRole, memberId} = do @@ -1882,9 +1882,9 @@ createSndMessages idsEvents = do encodeChatMessage maxEncodedMsgLength ChatMessage {chatVRange = vr, msgId = Just sharedMsgId, chatMsgEvent = evnt} groupMsgSigning :: GroupInfo -> ChatMsgEvent e -> Maybe MsgSigning -groupMsgSigning gInfo@GroupInfo {membership = GroupMember {memberId}, groupKeys = Just GroupKeys {groupRootKey, memberPrivKey}} evt +groupMsgSigning gInfo@GroupInfo {membership = GroupMember {memberId}, groupKeys = Just GroupKeys {sharedGroupId, memberPrivKey}} evt | useRelays' gInfo && requiresSignature (toCMEventTag evt) = - Just $ MsgSigning CBGroup (smpEncode (groupRootPubKey groupRootKey, memberId)) KRMember memberPrivKey + Just $ MsgSigning CBGroup (smpEncode (sharedGroupId, memberId)) KRMember memberPrivKey groupMsgSigning _ _ = Nothing sendGroupMemberMessages :: forall e. MsgEncodingI e => User -> GroupInfo -> Connection -> NonEmpty (ChatMsgEvent e) -> CM () diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 650b554857..697d2788bf 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -746,14 +746,16 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = _ -> messageError "CONF from invited member must have x.grp.acpt" GCHostMember -> case chatMsgEvent of - XGrpLinkInv glInv -> do - -- XGrpLinkInv here means we are connecting via prepared group, and we have to update user and host member records - (gInfo', m') <- withStore $ \db -> updatePreparedUserAndHostMembersInvited db vr user gInfo m glInv - -- [incognito] send saved profile - incognitoProfile <- forM customUserProfileId $ \pId -> withStore (\db -> getProfileById db userId pId) - let profileToSend = userProfileInGroup user gInfo (fromLocalProfile <$> incognitoProfile) - allowAgentConnectionAsync user conn' confId $ XInfo profileToSend - toView $ CEvtGroupLinkConnecting user gInfo' m' + XGrpLinkInv glInv@GroupLinkInvitation {groupProfile = GroupProfile {sharedGroupId = rcvGId}} + | let GroupInfo {groupProfile = GroupProfile {sharedGroupId = curGId}} = gInfo, rcvGId == curGId -> do + -- XGrpLinkInv here means we are connecting via prepared group, and we have to update user and host member records + (gInfo', m') <- withStore $ \db -> updatePreparedUserAndHostMembersInvited db vr user gInfo m glInv + -- [incognito] send saved profile + incognitoProfile <- forM customUserProfileId $ \pId -> withStore (\db -> getProfileById db userId pId) + let profileToSend = userProfileInGroup user gInfo (fromLocalProfile <$> incognitoProfile) + allowAgentConnectionAsync user conn' confId $ XInfo profileToSend + toView $ CEvtGroupLinkConnecting user gInfo' m' + | otherwise -> messageError "x.grp.link.inv: sharedGroupId mismatch" XGrpLinkReject glRjct@GroupLinkRejection {rejectionReason} -> do (gInfo', m') <- withStore $ \db -> updatePreparedUserAndHostMembersRejected db vr user gInfo m glRjct toView $ CEvtGroupLinkConnecting user gInfo' m' @@ -3054,8 +3056,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = toView $ CEvtGroupDeleted user gInfo'' {membership = membership {memberStatus = GSMemGroupDeleted}} m' msgSigned xGrpInfo :: GroupInfo -> GroupMember -> GroupProfile -> RcvMessage -> UTCTime -> CM (Maybe DeliveryJobScope) - xGrpInfo g@GroupInfo {groupProfile = p, businessChat} m@GroupMember {memberRole} p' msg@RcvMessage {msgSigned} brokerTs + xGrpInfo g@GroupInfo {groupProfile = p@GroupProfile {sharedGroupId = gId}, businessChat} m@GroupMember {memberRole} p'@GroupProfile {sharedGroupId = gId'} msg@RcvMessage {msgSigned} brokerTs | memberRole < GROwner = messageError "x.grp.info with insufficient member permissions" $> Nothing + | useRelays' g && gId' /= gId = messageError "x.grp.info: sharedGroupId cannot be changed" $> Nothing + | not (useRelays' g) && isJust gId' = messageError "x.grp.info: sharedGroupId not allowed in p2p groups" $> Nothing | otherwise = do case businessChat of Nothing -> unless (p == p') $ do @@ -3233,8 +3237,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = Just sm@SignedMsg {chatBinding, signatures, signedBody} | GroupMember {memberPubKey = Just pubKey, memberId} <- member -> case chatBinding of - CBGroup | Just GroupKeys {groupRootKey} <- groupKeys gInfo -> - let prefix = smpEncode chatBinding <> smpEncode (groupRootPubKey groupRootKey, memberId) + CBGroup | Just GroupKeys {sharedGroupId} <- groupKeys gInfo -> + let prefix = smpEncode chatBinding <> smpEncode (sharedGroupId, memberId) in signed MSSVerified <$ guard (all (\(MsgSignature KRMember sig) -> C.verify (C.APublicVerifyKey C.SEd25519 pubKey) sig (prefix <> signedBody)) signatures) _ -> signed MSSSignedNoKey <$ guard signatureOptional | otherwise -> signed MSSSignedNoKey <$ guard (signatureOptional || unverifiedAllowed membership member tag) @@ -3609,7 +3613,9 @@ runRelayRequestWorker a Worker {doWork} = do (FixedLinkData {linkEntityId, rootKey}, cData@(ContactLinkData _ UserContactData {owners})) <- getShortLinkConnReq NRMBackground user reqGroupLink liftIO (decodeLinkUserData cData) >>= \case Nothing -> throwChatError $ CEException "getLinkDataCreateRelayLink: no group link data" - Just GroupShortLinkData {groupProfile = gp} -> do + Just GroupShortLinkData {groupProfile = gp@GroupProfile {sharedGroupId}} -> do + unless ((B64UrlByteString <$> linkEntityId) == sharedGroupId) $ + throwChatError $ CEException "getLinkDataCreateRelayLink: linkEntityId does not match profile sharedGroupId" validateGroupProfile gp gVar <- asks random (_, memberPrivKey) <- liftIO $ atomically $ C.generateKeyPair gVar diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 8698b1718c..53ffbb878e 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -1459,7 +1459,8 @@ createRelayRequestGroup db vr user@User {userId} GroupRelayInvitation {fromMembe image = Nothing, groupLink = Nothing, groupPreferences = Nothing, - memberAdmission = Nothing + memberAdmission = Nothing, + sharedGroupId = Nothing } (groupId, _groupLDN) <- createGroup_ db userId placeholderProfile Nothing Nothing True (Just RSInvited) Nothing currentTs -- Store relay request data for recovery @@ -2227,7 +2228,7 @@ updateGroupProfileFromMember db user g@GroupInfo {groupId} Profile {displayName |] (Only groupId) toGroupProfile (displayName, fullName, shortDescr, description, image, groupLink, groupPreferences, memberAdmission) = - GroupProfile {displayName, fullName, shortDescr, description, image, groupLink, groupPreferences, memberAdmission} + GroupProfile {displayName, fullName, shortDescr, description, image, groupLink, groupPreferences, memberAdmission, sharedGroupId = Nothing} getGroupInfoByUserContactLinkConnReq :: DB.Connection -> VersionRangeChat -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe GroupInfo) getGroupInfoByUserContactLinkConnReq db vr user@User {userId} (cReqSchema1, cReqSchema2) = do diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index 0f25379204..c7031528ac 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -676,11 +676,12 @@ toGroupInfo vr userContactId chatTags ((groupId, localDisplayName, displayName, let membership = (toGroupMember userContactId userMemberRow) {memberChatVRange = vr} chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite} fullGroupPreferences = mergeGroupPreferences groupPreferences - groupProfile = GroupProfile {displayName, fullName, shortDescr, description, image, groupPreferences, memberAdmission, groupLink} + groupKeys = toGroupKeys groupKeysRow + sharedGroupId = (\GroupKeys {sharedGroupId = gId} -> gId) <$> groupKeys + groupProfile = GroupProfile {displayName, fullName, shortDescr, description, image, groupPreferences, memberAdmission, groupLink, sharedGroupId} businessChat = toBusinessChatInfo businessRow preparedGroup = toPreparedGroup preparedGroupRow groupSummary = GroupSummary {currentMembers, publicMemberCount} - groupKeys = toGroupKeys groupKeysRow in GroupInfo {groupId, useRelays = BoolDef useRelays, relayOwnStatus, localDisplayName, groupProfile, localAlias, businessChat, fullGroupPreferences, membership, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, preparedGroup, chatTags, chatItemTTL, uiThemes, groupSummary, customData, membersRequireAttention, viaGroupLinkUri, groupKeys} toPreparedGroup :: PreparedGroupRow -> Maybe PreparedGroup diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 32d1c726bf..584e46a226 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -151,7 +151,7 @@ data NewUser = NewUser newtype B64UrlByteString = B64UrlByteString ByteString deriving (Eq, Show) - deriving newtype (FromField) + deriving newtype (FromField, Encoding) instance ToField B64UrlByteString where toField (B64UrlByteString m) = toField $ Binary m @@ -764,7 +764,8 @@ data GroupProfile = GroupProfile image :: Maybe ImageData, groupLink :: Maybe ShortLinkContact, groupPreferences :: Maybe GroupPreferences, - memberAdmission :: Maybe GroupMemberAdmission + memberAdmission :: Maybe GroupMemberAdmission, + sharedGroupId :: Maybe B64UrlByteString -- group identity = sha256(genesis root key), immutable } deriving (Eq, Show) diff --git a/tests/ProtocolTests.hs b/tests/ProtocolTests.hs index a01bce6418..da6c3b9026 100644 --- a/tests/ProtocolTests.hs +++ b/tests/ProtocolTests.hs @@ -107,7 +107,7 @@ testProfile :: Profile testProfile = Profile {displayName = "alice", fullName = "Alice", shortDescr = Nothing, image = Just (ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII="), peerType = Nothing, contactLink = Nothing, preferences = testChatPreferences} testGroupProfile :: GroupProfile -testGroupProfile = GroupProfile {displayName = "team", fullName = "Team", description = Nothing, shortDescr = Nothing, image = Nothing, groupLink = Nothing, groupPreferences = testGroupPreferences, memberAdmission = Nothing} +testGroupProfile = GroupProfile {displayName = "team", fullName = "Team", description = Nothing, shortDescr = Nothing, image = Nothing, groupLink = Nothing, groupPreferences = testGroupPreferences, memberAdmission = Nothing, sharedGroupId = Nothing} decodeChatMessageTest :: Spec decodeChatMessageTest = describe "Chat message encoding/decoding" $ do