From 388fe39180b0296f43aec3e22248419c10c574fc Mon Sep 17 00:00:00 2001 From: "Evgeny @ SimpleX Chat" <259188159+evgeny-simplex@users.noreply.github.com> Date: Mon, 5 Oct 2026 16:13:54 +0000 Subject: [PATCH] prevent fallback to PHTest --- cabal.project | 2 +- ...026-09-04-badge-binding-and-file-limits.md | 39 ++++++++--------- scripts/nix/sha256map.nix | 2 +- src/Simplex/Chat/Badges.hs | 9 +++- src/Simplex/Chat/Library/Commands.hs | 43 ++++++++----------- src/Simplex/Chat/Library/Internal.hs | 18 +++----- src/Simplex/Chat/Library/Subscriber.hs | 7 +-- .../SQLite/Migrations/agent_query_plans.txt | 7 +++ 8 files changed, 64 insertions(+), 63 deletions(-) diff --git a/cabal.project b/cabal.project index 15cf21ea98..f97ea33002 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: f66d9ec3952f0ff21c3af5827d05c6d596dad110 + tag: c7575ddfd2c0196b6eb1f9d0c1c59988c02dfb6d source-repository-package type: git diff --git a/plans/2026-09-04-badge-binding-and-file-limits.md b/plans/2026-09-04-badge-binding-and-file-limits.md index 899ba8305a..64500e669a 100644 --- a/plans/2026-09-04-badge-binding-and-file-limits.md +++ b/plans/2026-09-04-badge-binding-and-file-limits.md @@ -293,7 +293,7 @@ An address is a contact address, a business address, or a group link. The link k ### 14.1 Agent: verification codes -Merged in simplexmq `5294b7d8`. +Merged in simplexmq `5294b7d8`, except the ratchet stored by `newConnToAcceptDR`, its use in `startJoinInvitationDR`, and their tests. **Second verification code.** `RatchetInitParams` gains `rcVerifyCodePQ` and `Ratchet` gains `rcVCPQ :: Maybe Str`. The code is derived in `pqX3dh` with a separate HKDF over the same inputs, info `SimpleXVerifyCode`, 32 bytes; the ratchet keys and `rcAD` are unchanged. The code is exported keying material over every handshake input, the KEM included — the fourth of the paper's mitigations. A ratchet created before this change is decoded with `rcVCPQ = Nothing`, and its code is set at the next ratchet resync. The chat uses the AD code, `codeAD`, for the security code and for badge bindings; `codePQ` is stored. @@ -331,11 +331,11 @@ data ContactRequestBinding = CRBRatchet ConnVerifyCodes | CRBRequest ByteString - `CRContactUri` with ratchet keys: the same, from the address keys. - `CRContactUri` without keys: the x3dh keys are generated and stored (`generateRcvE2EParams`, `createRatchetX3dhKeys`); the binding is `CRBRequest (sha256 (smpEncode (k1, k2, kem, senderId)))` — the request's public keys and the queue id from the link's `SMPQueueUri`. -The same pair is returned by `prepareConnectionToAccept`: for `CRInvitation` the ratchet is created from the invitation's keys, for `CRInvitationDR` the ratchet stored in the invitation is used. In `startJoinInvitation` the ratchet is read before one is created, as in the contact path and its retry branch; in `createConnReq` the x3dh keys are read before they are generated, as in `mkJoinInvitation`. The stored ratchet is used by async joins and accepts. The prepare step is local. +The same pair is returned by `prepareConnectionToAccept`: for `CRInvitation` the ratchet is created from the invitation's keys, for `CRInvitationDR` the ratchet in the invitation is stored with the connection by `newConnToAcceptDR`. In `startJoinInvitation` and `startJoinInvitationDR` the stored ratchet is used, and a ratchet is created only for a connection without one, as in the contact path and its retry branch; in `createConnReq` the x3dh keys are read before they are generated, as in `mkJoinInvitation`. The stored ratchet is used by async joins and accepts. The prepare step is local. **Events.** A `ContactRequestBinding` field in `REQ`: `CRBRequest` in `smpInvitation`, computed from the received `CRInvitationUri` and the queue of the request; `CRBRatchet` in `smpContactRequest`, from the ratchet initialised there. `CONF` and `INFO` are unchanged: the receiver's ratchet is stored before the notification, so its codes are available to `getConnectionVerifyCodes`. -**Tests.** `DoubleRatchetTests`: the parties agree on `rcVerifyCodePQ`, a substituted KEM key makes it differ while `assocData` matches, and a ratchet stored before the change decodes with `rcVCPQ = Nothing`. `FunctionalAPITests`: both peers get the same codes, and codes cleared from a row are recomputed and saved on the next read. +**Tests.** `DoubleRatchetTests`: the parties agree on `rcVerifyCodePQ`, a substituted KEM key makes it differ while `assocData` matches, and a ratchet stored before the change decodes with `rcVCPQ = Nothing`. `FunctionalAPITests`: both peers get the same codes, and codes cleared from a row are recomputed and saved on the next read. For an address with ratchet keys, the codes of the requester's prepare step, of `REQ`, of the acceptor's prepare step and of the stored ratchet are equal before `acceptContact`; an accept of a prepared connection without a ratchet is completed by `acceptContact`. ### 14.2 Agent: links before link data @@ -399,7 +399,7 @@ prepareConnShortLink :: AgentClient -> ConnId -> Maybe CRClientData -> AE (ConnS ### 14.3 Chat -In implementation order. At every direct send, a `Nothing` from `connPresHeader`, a header missing from the `connsPresHeaders` map, and a retry without a stored request header are replaced with `PHTest` by `sndPresHeader`. At a send into a group, no badge is presented for a `Nothing` from `groupPresHeader`. +In implementation order. A badge is presented only with a header: no badge is presented at a direct send for a connection without codes or for a retry without a stored request header, and at a send into a group for a `Nothing` from `groupPresHeader`. An agent error is returned as a chat error. **1. Headers** — `Badges.hs` @@ -449,9 +449,8 @@ With `Nothing`, no badge is presented. A badge is presented only when `presentsU - `memberPresHeader :: GroupInfo -> MemberId -> Maybe C.PublicKeyEd25519 -> Maybe ProofPresHeader` — `PHChat (encodeChatBinding CBGroup (smpEncode (publicGroupId, memberId, key)))` in a channel, `PHChat (encodeChatBinding CBGroup (smpEncode (memberId, key)))` in a p2p group, `Nothing` without a key - `memberInfoPresHeader :: GroupInfo -> MemberInfo -> Maybe ProofPresHeader` — in a channel `memberPresHeader` of the id and key in the `MemberInfo`; `Nothing` in a p2p group - `relayInvPresHeader :: GroupRelayInvitation -> Maybe ProofPresHeader` — the channel header of the owner's id and key in the invitation (item 12) -- `sndPresHeader :: Maybe ProofPresHeader -> CM ProofPresHeader` — the header, or `PHTest` of 16 random bytes for `Nothing` -- `connPresHeader :: Connection -> CM (Maybe ProofPresHeader)` — `directPresHeader . CRBRatchet` of `getConnectionVerifyCodes`; `Nothing` on error -- `connsPresHeaders :: [Connection] -> CM (Map ConnId ProofPresHeader)` — the same from `getConnectionsVerifyCodes` +- `connPresHeader :: Connection -> CM (Maybe ProofPresHeader)` — `connsPresHeaders` of one connection +- `connsPresHeaders :: [Connection] -> CM (Map ConnId ProofPresHeader)` — `directPresHeader . CRBRatchet` of `getConnectionsVerifyCodes`; the map includes the connections with codes **5. The binding from prepare steps** @@ -476,24 +475,24 @@ ALTER TABLE connections ADD COLUMN pres_header BLOB; **7. Direct sends** -- `joinContact` (`Commands.hs:3979`): the request header, `ProofPresHeader`, is a parameter, set in `connect'`, `joinPreparedConn'` and `connectContactViaAddress` — the `prepareContact` header for a new connection, the stored header for a retry. The badge is presented with `groupPresHeader` in a relay group, and with the request header otherwise. -- `connectViaInvitation` (`Commands.hs:3801-3833`) and `connectMemberContact` (`:3388-3415`): the prepare binding for a new connection; `connPresHeader` for a prepared one. -- `joinMemberContactAsync` (`Subscriber.hs:3947`): the header is a parameter, set in `xGrpDirectInv` to the `prepareAgentJoin` header. -- `acceptContactRequest` (`Internal.hs:974-1005`): the prepare binding for a new connection; `connPresHeader` for an existing one. -- `acceptContactRequestAsync` (`Internal.hs:1007-1026`): the profile is built after `prepareAgentAccept`, from its header. -- `CONF` replies (`Subscriber.hs:509` direct case, `:628`) and `updateContactPrefs` (`Commands.hs:4108`): `connPresHeader`. -- `sendUpdateToContacts` (`Commands.hs:4039-4077`) and `presentUserBadgeToContacts` (`:5212-5228`): one `connsPresHeaders` call per command, when `presentsUserBadge` holds. +- `joinContact` (`Commands.hs:3982`): the request header, `Maybe ProofPresHeader`, is a parameter, set in `connect'`, `joinPreparedConn'` and `connectContactViaAddress` — the `prepareContact` header for a new connection, the stored header for a retry. The badge is presented with `groupPresHeader` in a relay group, and with the request header otherwise. +- `connectViaInvitation` (`Commands.hs:3808-3845`) and `connectMemberContact` (`:3395-3422`): the prepare binding for a new connection; `connPresHeader` for a prepared one. +- `joinMemberContactAsync` (`Subscriber.hs:3956`): the header is a parameter, set in `xGrpDirectInv` to the `prepareAgentJoin` header. +- `acceptContactRequest` (`Internal.hs:984-1014`): the prepare binding for a new connection; `connPresHeader` for an existing one. +- `acceptContactRequestAsync` (`Internal.hs:1016-1035`): the profile is built after `prepareAgentAccept`, from its header. +- `CONF` replies (`Subscriber.hs:507` direct case, `:626`) and `updateContactPrefs` (`Commands.hs:4099`): `connPresHeader`. +- `sendUpdateToContacts` (`Commands.hs:4043-4082`) and `presentUserBadgeToContacts` (`:5216-5231`): one `connsPresHeaders` call per command, when `presentsUserBadge` holds. **8. Direct receipts** -- `REQ` (`Subscriber.hs:1404`): `directPresHeader` of the `REQ` binding is a parameter of `profileContactRequest`, passed to `createOrUpdateContactRequest` and to `acceptGroupJoinRequestAsync`. -- `processContactProfileUpdate` (`Subscriber.hs:2771`) and `saveConnInfo` (`:3180`, for `createDirectContact`): the header is a parameter, `connPresHeader` of the connection, read by the caller. In the direct case, the badge in the `CONF` reply is presented with the same header. +- `REQ` (`Subscriber.hs:1403`): `directPresHeader` of the `REQ` binding is a parameter of `profileContactRequest`, passed to `createOrUpdateContactRequest` and to `acceptGroupJoinRequestAsync`. +- `processContactProfileUpdate` (`Subscriber.hs:2767`) and `saveConnInfo` (`:3189`, for `createDirectContact`): the header is a parameter, `connPresHeader` of the connection, read by the caller. In the direct case, the badge in the `CONF` reply is presented with the same header. **9. Groups** -- `groupPresHeader` at every send into a group: `Commands.hs:3998` (`joinContact`, relay group), `:4329`; `Subscriber.hs:506` group case, `:642`, `:836`, `:978`, `:1260`, and `membershipHandshakeProfile` (`:3379-3384`) for `:799`, `:850` and `:3366`; `Internal.hs:2530` (`encodeXGrpAcpt`) and `:2715`. -- `acceptGroupJoinRequestAsync` (`Internal.hs:1028`): the expected header is a new parameter, passed to `createJoiningMember` and `updateMemberProfile` — in `memberJoinRequestViaRelay`, `memberPresHeader` of the joining member's id and the key in `XMember` when the message signature is verified with it. `Nothing` is passed to `createJoiningMember` in `acceptGroupJoinSendRejectAsync`. -- Host `INFO` with `XInfo` (`Subscriber.hs:865-872`): after `storeMemberKey`, the profile is stored by `processMemberProfileUpdate` with `signedMemberPresHeader`, under the member's key. +- `groupPresHeader` at every send into a group: `Commands.hs:4002` (`joinContact`, relay group), `:4332`; `Subscriber.hs:506` group case, `:639`, `:833`, `:975`, `:1257`, and `membershipHandshakeProfile` (`:3388-3393`) for `:796`, `:847` and `:3375`; `Internal.hs:2535` (`encodeXGrpAcpt`) and `:2719`. +- `acceptGroupJoinRequestAsync` (`Internal.hs:1037`): the expected header is a new parameter, passed to `createJoiningMember` and `updateMemberProfile` — in `memberJoinRequestViaRelay`, `memberPresHeader` of the joining member's id and the key in `XMember` when the message signature is verified with it. `Nothing` is passed to `createJoiningMember` in `acceptGroupJoinSendRejectAsync`. +- Host `INFO` with `XInfo` (`Subscriber.hs:862-869`): after `storeMemberKey`, the profile is stored by `processMemberProfileUpdate` with `signedMemberPresHeader`, under the member's key. - The profile is also stored by `processMemberProfileUpdate` when the received proof is accepted and differs from the stored member proof in its header or its disclosed information, and when a profile without a proof is received for a member with a stored proof. - Introductions: - In `memberInfo` (`Internal.hs:1335`) the stored proof (item 11) is included when `acceptedProof` holds under `memberPresHeader` of the member's id and stored key in a channel, and under `Nothing` in a p2p group: in a p2p group only a `PHTest` proof is included. @@ -589,7 +588,7 @@ Postgres: `BIGINT`, `BYTEA` and `TIMESTAMPTZ`. Both schema dumps and `chat_query - Channel: a relay invitation with an owner key that differs from the link data is failed by the relay, and the relay stays invited (`testChannelAddRelayOwnerKeyMismatch`). - Link data: the badge is shown from an invitation link under `PHLink`, an address, and an address that gets its first short link. -`PHTest` is still sent by released clients and, through `sndPresHeader`, on a retry of a connection prepared before this change. +`PHTest` proofs are generated only by released clients. For a connection prepared by a released client and joined after the update, the badge is presented only when its ratchet was stored by an earlier attempt; a retried request to an address prepared by a released client is sent without a badge. ## Out of scope diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 8e3dad21c8..65a44b4595 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."f66d9ec3952f0ff21c3af5827d05c6d596dad110" = "1dfz8f5fcr5dzyc15m523zl932sxdrch8snhl4vj9zgw9bgyrirx"; + "https://github.com/simplex-chat/simplexmq.git"."c7575ddfd2c0196b6eb1f9d0c1c59988c02dfb6d" = "0qy9jjg6vizh77vhm4xvnb7sfqpmb7d53zspmk7a4mkbv48354kz"; "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/Badges.hs b/src/Simplex/Chat/Badges.hs index e9308fb186..0ee5d8a63e 100644 --- a/src/Simplex/Chat/Badges.hs +++ b/src/Simplex/Chat/Badges.hs @@ -272,7 +272,14 @@ maxSndXFTPFileSize lims now = \case -- presentation, not bound to any context. -- PHUnknown is the forward-compat catch-all for tags this version does not interpret. -data ProofPresHeaderTag = PHTestTag | PHChatTag | PHFileInvTag | PHFileDescrTag | PHRequestTag | PHLinkTag | PHUnknownTag Char +data ProofPresHeaderTag + = PHTestTag -- random nonce: released clients + | PHChatTag -- direct chat ratchet, group member key + | PHFileInvTag -- file invitation + | PHFileDescrTag -- file description + | PHRequestTag -- request to address without ratchet keys + | PHLinkTag -- invitation and address link data + | PHUnknownTag Char -- tags of newer versions instance StrEncoding ProofPresHeaderTag where strEncode = B.singleton . \case diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 89ad3c84c8..8e9ef209c2 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -3401,7 +3401,7 @@ processChatCommand cxt nm = \case -- so incognito profile can be attached to it and be visible in UI before accepting Nothing -> joinNewConn subMode Just conn@Connection {connStatus} -> case connStatus of - ConnPrepared -> joinPreparedConn subMode conn =<< sndPresHeader =<< connPresHeader conn + ConnPrepared -> joinPreparedConn subMode conn =<< connPresHeader conn _ -> throwChatError $ CEException "connection already started (past prepared status)" where joinNewConn subMode = do @@ -3412,10 +3412,10 @@ processChatCommand cxt nm = \case conn <- withStore $ \db -> do connId <- liftIO $ createMemberContactConn db user acId Nothing gInfo mConn ConnPrepared contactId subMode getConnectionById db cxt user connId - joinPreparedConn subMode conn $ directPresHeader binding - joinPreparedConn subMode conn presHeader = do + joinPreparedConn subMode conn $ Just $ directPresHeader binding + joinPreparedConn subMode conn presHeader_ = do -- [incognito] send membership incognito profile - p <- presentUserBadge user (incognitoMembershipProfile gInfo) (Just presHeader) $ userProfileDirect user (fromLocalProfile <$> incognitoMembershipProfile gInfo) Nothing True + p <- presentUserBadge user (incognitoMembershipProfile gInfo) presHeader_ $ userProfileDirect user (fromLocalProfile <$> incognitoMembershipProfile gInfo) Nothing True dm <- encodeConnInfo $ XInfo p Nothing sqSecured <- withAgent $ \a -> joinConnection a nm (aUserId user) (aConnId conn) True cReq dm PQSupportOff subMode let newStatus = if sqSecured then ConnSndReady else ConnJoined @@ -3820,8 +3820,7 @@ processChatCommand cxt nm = \case | connStatus == ConnNew && contactConnInitiated -> joinNewConn chatV -- own connection link | connStatus == ConnPrepared -> do -- retrying join after error localIncognitoProfile <- forM customUserProfileId $ \pId -> withFastStore $ \db -> getProfileById db userId pId - presHeader <- sndPresHeader =<< connPresHeader conn - joinPreparedConn conn (fromLocalProfile <$> localIncognitoProfile) presHeader + joinPreparedConn conn (fromLocalProfile <$> localIncognitoProfile) =<< connPresHeader conn Just ent -> throwCmdError $ "connection is not RcvDirectMsgConnection: " <> show (connEntityInfo ent) where -- all supported versions support PQ encryption @@ -3832,9 +3831,9 @@ processChatCommand cxt nm = \case (connId, binding) <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq pqSup' let ccLink = CCLink cReq $ serverShortLink <$> sLnk_ conn <- withFastStore' $ \db -> createDirectConnection' db userId connId ccLink contactId_ ConnPrepared incognitoProfile subMode chatV pqSup' - joinPreparedConn conn incognitoProfile $ directPresHeader binding - joinPreparedConn conn incognitoProfile presHeader = do - profileToSend <- presentUserBadge user incognitoProfile (Just presHeader) $ userProfileDirect user incognitoProfile Nothing True + joinPreparedConn conn incognitoProfile $ Just $ directPresHeader binding + joinPreparedConn conn incognitoProfile presHeader_ = do + profileToSend <- presentUserBadge user incognitoProfile presHeader_ $ userProfileDirect user incognitoProfile Nothing True dm <- encodeConnInfoPQ pqSup' $ XInfo profileToSend Nothing sqSecured <- withAgent $ \a -> joinConnection a nm (aUserId user) (aConnId conn) True cReq dm pqSup' subMode let newStatus = if sqSecured then ConnSndReady else ConnJoined @@ -3889,9 +3888,8 @@ processChatCommand cxt nm = \case -- TODO [relays] member: refactor joinContact and up avoiding parallel ifs, xContactId is not used xContactId <- mkXContactId xContactId_ ((cReq', reqHeader_), localIncognitoProfile) <- withFastStore $ \db -> (,) <$> getConnReqContact db connId <*> forM customUserProfileId (getProfileById db userId) - reqHeader <- sndPresHeader reqHeader_ let incognitoProfile = fromLocalProfile <$> localIncognitoProfile - conn' <- joinContact user conn cReq' incognitoProfile reqHeader xContactId welcomeSharedMsgId msg_ gInfo_ relayMemberId_ PQSupportOn + conn' <- joinContact user conn cReq' incognitoProfile reqHeader_ xContactId welcomeSharedMsgId msg_ gInfo_ relayMemberId_ PQSupportOn pure $ CVRSentInvitation conn' incognitoProfile connect' groupLinkId xContactId_ gInfo_ = do let inGroup = isJust groupLinkId @@ -3906,7 +3904,7 @@ processChatCommand cxt nm = \case subMode <- chatReadVar subscriptionMode let sLnk' = serverShortLink <$> sLnk conn <- withFastStore' $ \db -> createConnReqConnection db userId connId preparedEntity_ cReq reqHeader cReqHash1 sLnk' xContactId incognitoProfile_ groupLinkId subMode chatV pqSup - conn' <- joinContact user conn cReq incognitoProfile reqHeader xContactId welcomeSharedMsgId msg_ gInfo_ relayMemberId_ pqSup + conn' <- joinContact user conn cReq incognitoProfile (Just reqHeader) xContactId welcomeSharedMsgId msg_ gInfo_ relayMemberId_ pqSup pure $ CVRSentInvitation conn' incognitoProfile connectContactViaAddress :: User -> IncognitoEnabled -> Contact -> CreatedLinkContact -> CM ChatResponse connectContactViaAddress user@User {userId} incognito ct@Contact {contactId, activeConn} (CCLink cReq shortLink) = @@ -3921,7 +3919,7 @@ processChatCommand cxt nm = \case subMode <- chatReadVar subscriptionMode let cReqHash = contactCReqHash cReq conn <- withFastStore' $ \db -> createConnReqConnection db userId connId (Just $ PCEContact ct) cReq reqHeader cReqHash shortLink newXContactId (NewIncognito <$> incognitoProfile) Nothing subMode chatV pqSup - void $ joinContact user conn cReq incognitoProfile reqHeader newXContactId Nothing Nothing Nothing Nothing pqSup + void $ joinContact user conn cReq incognitoProfile (Just reqHeader) newXContactId Nothing Nothing Nothing Nothing pqSup ct' <- withStore $ \db -> getContact db cxt user contactId pure $ CRSentInvitationToContact user ct' incognitoProfile Just conn@Connection {connId, connStatus, xContactId = xContactId_, customUserProfileId} -> case connStatus of @@ -3929,9 +3927,8 @@ processChatCommand cxt nm = \case when (incognito /= isJust customUserProfileId) $ throwCmdError "incognito mode is different from prepared connection" xContactId <- mkXContactId xContactId_ ((cReq', reqHeader_), localIncognitoProfile) <- withFastStore $ \db -> (,) <$> getConnReqContact db connId <*> forM customUserProfileId (getProfileById db userId) - reqHeader <- sndPresHeader reqHeader_ let incognitoProfile = fromLocalProfile <$> localIncognitoProfile - void $ joinContact user conn cReq' incognitoProfile reqHeader xContactId Nothing Nothing Nothing Nothing PQSupportOn + void $ joinContact user conn cReq' incognitoProfile reqHeader_ xContactId Nothing Nothing Nothing Nothing PQSupportOn ct' <- withStore $ \db -> getContact db cxt user contactId pure $ CRSentInvitationToContact user ct' incognitoProfile _ -> throwCmdError "contact already has connection" @@ -3982,8 +3979,8 @@ processChatCommand cxt nm = \case pure (connId, chatV, directPresHeader binding) mkXContactId :: Maybe XContactId -> CM XContactId mkXContactId = maybe (XContactId <$> drgRandomBytes 16) pure - joinContact :: User -> Connection -> ConnReqContact -> Maybe Profile -> ProofPresHeader -> XContactId -> Maybe SharedMsgId -> Maybe (SharedMsgId, MsgContent) -> Maybe (Maybe GroupInfoKeys) -> Maybe MemberId -> PQSupport -> CM Connection - joinContact user conn cReq incognitoProfile reqHeader xContactId welcomeSharedMsgId msg_ gInfo_ relayMemberId_ pqSup = do + joinContact :: User -> Connection -> ConnReqContact -> Maybe Profile -> Maybe ProofPresHeader -> XContactId -> Maybe SharedMsgId -> Maybe (SharedMsgId, MsgContent) -> Maybe (Maybe GroupInfoKeys) -> Maybe MemberId -> PQSupport -> CM Connection + joinContact user conn cReq incognitoProfile reqHeader_ xContactId welcomeSharedMsgId msg_ gInfo_ relayMemberId_ pqSup = do -- gInfo_ is Maybe (Maybe GroupInfo), where Just Nothing means "some unknown group", e.g. when joining via link without profile profileToSend <- presentUserBadge user incognitoProfile presHeader_ $ case gInfo_ of @@ -4003,7 +4000,7 @@ processChatCommand cxt nm = \case where presHeader_ = case gInfo_ of Just (Just (GIK g _)) | useRelays' g -> groupPresHeader g - _ -> Just reqHeader + _ -> reqHeader_ contactMember :: Contact -> [GroupMember] -> Maybe GroupMember contactMember Contact {contactId} = find $ \GroupMember {memberContactId = cId, memberStatus = s} -> @@ -4077,8 +4074,7 @@ processChatCommand cxt nm = \case -- non-incognito (filtered above), so the user's badge is presented; a profile update keeps the badge instead of clearing it ctSndEvent :: Map ConnId ProofPresHeader -> ChangedProfileContact -> CM (ConnOrGroupId, Maybe MsgSigning, ChatMsgEvent 'Json) ctSndEvent presHeaders ChangedProfileContact {mergedProfile', conn = conn@Connection {connId}} = do - presHeader <- sndPresHeader $ M.lookup (aConnId conn) presHeaders - p'' <- presentUserBadge user' Nothing (Just presHeader) mergedProfile' + p'' <- presentUserBadge user' Nothing (M.lookup (aConnId conn) presHeaders) mergedProfile' pure (ConnectionId connId, Nothing, XInfo p'' Nothing) ctMsgReq :: ChangedProfileContact -> Either ChatError SndMessage -> Either ChatError ChatMsgReq ctMsgReq ChangedProfileContact {conn} = @@ -4112,8 +4108,8 @@ processChatCommand cxt nm = \case mergedProfile' = userProfileDirect user (fromLocalProfile <$> incognitoProfile) (Just ct') False when (mergedProfile' /= mergedProfile) $ withContactLock "updateContactPrefs" (contactId' ct) $ do - presHeader <- sndPresHeader =<< connPresHeader conn - p <- presentUserBadge user incognitoProfile (Just presHeader) mergedProfile' + presHeader_ <- connPresHeader conn + p <- presentUserBadge user incognitoProfile presHeader_ mergedProfile' void (sendDirectContactMessage user ct' $ XInfo p Nothing) `catchAllErrors` eToView lift . when (directOrUsed ct') $ createSndFeatureItems user ct ct' pure $ CRContactPrefsUpdated user ct ct' @@ -5231,8 +5227,7 @@ presentUserBadgeToContacts user'@User {userId, profile = LocalProfile {localBadg presHeaders <- if presentsUserBadge user' then connsPresHeaders $ map snd sendConns else pure M.empty withChatLock "presentUserBadge" $ forM_ sendConns $ \(ct, conn) -> do let ct' = updateMergedPreferences user' ct - presHeader <- sndPresHeader $ M.lookup (aConnId conn) presHeaders - p <- presentUserBadge user' Nothing (Just presHeader) $ userProfileDirect user' Nothing (Just ct') False + p <- presentUserBadge user' Nothing (M.lookup (aConnId conn) presHeaders) $ userProfileDirect user' Nothing (Just ct') False void (sendDirectContactMessage user' ct' (XInfo p Nothing)) `catchAllErrors` eToView -- | The check character is verified before anything leaves the device, and the signing keys are diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index 205d8f9c28..df12059921 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -988,13 +988,13 @@ acceptContactRequest nm user@User {userId} UserContactRequest {agentInvitationId pqSup' = pqSup `CR.pqSupportAnd` pqSupport cxt <- chatStoreCxt let chatV = vr cxt `peerConnChatVersion` cReqChatVRange - (ct, conn, incognitoProfile, presHeader) <- case contactId_ of + (ct, conn, incognitoProfile, presHeader_) <- case contactId_ of Nothing -> do incognitoProfile <- if incognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing (connId, binding) <- withAgent $ \a -> prepareConnectionToAccept a (aUserId user) True invId pqSup' (ct, conn) <- withStore' $ \db -> createContactFromRequest db user userContactLinkId_ connId chatV cReqChatVRange cName profileId cp xContactId incognitoProfile subMode pqSup' False - pure (ct, conn, incognitoProfile, directPresHeader binding) + pure (ct, conn, incognitoProfile, Just $ directPresHeader binding) Just contactId -> do ct <- withFastStore $ \db -> getContact db cxt user contactId case contactConn ct of @@ -1005,12 +1005,11 @@ acceptContactRequest nm user@User {userId} UserContactRequest {agentInvitationId conn <- withStore' $ \db -> do forM_ xContactId $ \xcId -> setContactAcceptedXContactId db ct xcId createAcceptedContactConn db user userContactLinkId_ contactId connId chatV cReqChatVRange pqSup' incognitoProfile subMode currentTs - pure (ct {activeConn = Just conn} :: Contact, conn, incognitoProfile, directPresHeader binding) + pure (ct {activeConn = Just conn} :: Contact, conn, incognitoProfile, Just $ directPresHeader binding) Just conn@Connection {customUserProfileId} -> do incognitoProfile <- forM customUserProfileId $ \pId -> withFastStore $ \db -> getProfileById db userId pId - presHeader <- sndPresHeader =<< connPresHeader conn - pure (ct, conn, ExistingIncognito <$> incognitoProfile, presHeader) - profileToSend <- presentUserBadge user incognitoProfile (Just presHeader) $ userProfileDirect user (fromIncognitoProfile <$> incognitoProfile) (Just ct) True + (ct, conn, ExistingIncognito <$> incognitoProfile,) <$> connPresHeader conn + profileToSend <- presentUserBadge user incognitoProfile presHeader_ $ userProfileDirect user (fromIncognitoProfile <$> incognitoProfile) (Just ct) True dm <- encodeConnInfoPQ pqSup' $ XInfo profileToSend Nothing (ct,conn,) <$> withAgent (\a -> acceptContact a nm (aUserId user) (aConnId conn) True invId dm pqSup' subMode) @@ -2281,14 +2280,11 @@ relayInvPresHeader :: GroupRelayInvitation -> Maybe ProofPresHeader relayInvPresHeader GroupRelayInvitation {fromMember = MemberIdRole {memberId}, publicGroupId, fromMemberKey} = (\gId (MemberKey k) -> PHChat $ encodeChatBinding CBGroup $ smpEncode (gId, memberId, k)) <$> publicGroupId <*> fromMemberKey -sndPresHeader :: Maybe ProofPresHeader -> CM ProofPresHeader -sndPresHeader = maybe (PHTest <$> drgRandomBytes 16) pure - connPresHeader :: Connection -> CM (Maybe ProofPresHeader) -connPresHeader conn = eitherToMaybe <$> tryAllErrors (directPresHeader . CRBRatchet <$> withAgent (`getConnectionVerifyCodes` aConnId conn)) +connPresHeader conn = M.lookup (aConnId conn) <$> connsPresHeaders [conn] connsPresHeaders :: [Connection] -> CM (Map ConnId ProofPresHeader) -connsPresHeaders conns = either (const M.empty) (M.map (directPresHeader . CRBRatchet)) <$> tryAllErrors (withAgent (`getConnectionsVerifyCodes` map aConnId conns)) +connsPresHeaders conns = M.map (directPresHeader . CRBRatchet) <$> withAgent (`getConnectionsVerifyCodes` map aConnId conns) -- receiving side of contact/invitation link data: verify the badge proof from the link profile -- and set the crypto-free display badge for the UI (the raw proof stays in profile for APIPrepareContact) diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 04aa26c720..8e37783f2f 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -504,9 +504,7 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId) profileToSend <- case gInfo_ of Just (GIK gInfo _) -> presentUserBadge user incognitoProfile (groupPresHeader gInfo) $ userProfileInGroup user gInfo (fromLocalProfile <$> incognitoProfile) - Nothing -> do - presHeader <- sndPresHeader presHeader_ - presentUserBadge user incognitoProfile (Just presHeader) $ userProfileDirect user (fromLocalProfile <$> incognitoProfile) Nothing True + Nothing -> presentUserBadge user incognitoProfile presHeader_ $ userProfileDirect user (fromLocalProfile <$> incognitoProfile) Nothing True -- [async agent commands] no continuation needed, but command should be asynchronous for stability allowAgentConnectionAsync user conn'' confId gInfo_ $ XInfo profileToSend ((\(GIK _ gks) -> groupMemberKey gks) <$> gInfo_) INFO pqSupport connInfo -> do @@ -625,8 +623,7 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag ct' <- processContactProfileUpdate ct presHeader_ profile False `catchAllErrors` const (pure ct) -- [incognito] send incognito profile incognitoProfile <- forM customUserProfileId $ \profileId -> withStore $ \db -> getProfileById db userId profileId - presHeader <- sndPresHeader presHeader_ - p <- presentUserBadge user incognitoProfile (Just presHeader) $ userProfileDirect user (fromLocalProfile <$> incognitoProfile) (Just ct') True + p <- presentUserBadge user incognitoProfile presHeader_ $ userProfileDirect user (fromLocalProfile <$> incognitoProfile) (Just ct') True allowAgentConnectionAsync user conn'' confId Nothing $ XInfo p Nothing void $ withStore' $ \db -> resetMemberContactFields db ct' XGrpLinkInv glInv -> do diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt index 833a1ae602..1eeb137603 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt @@ -671,6 +671,13 @@ Query: Plan: +Query: + INSERT INTO ratchets (conn_id, ratchet_state, rc_verify_code_ad, rc_verify_code_pq) + VALUES (?, ?, ?, ?) + ON CONFLICT (conn_id) DO NOTHING + +Plan: + Query: INSERT INTO ratchets (conn_id, ratchet_state, rc_verify_code_ad, rc_verify_code_pq) VALUES (?, ?, ?, ?)