diff --git a/cabal.project b/cabal.project index 2614f4332d..9d5f1b8e21 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: ee66dd68d4429e769da7f2506a48af5887e0f7af + tag: a2f985dc68f870c0327fc3e871f2816828bcba2d 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 2783064a88..29d6905b85 100644 --- a/plans/2026-09-04-badge-binding-and-file-limits.md +++ b/plans/2026-09-04-badge-binding-and-file-limits.md @@ -343,45 +343,65 @@ Files: `Agent.hs`, `Agent/Client.hs`, `Agent/Protocol.hs`, `Crypto/ShortLink.hs` The chat pins the simplexmq commit in `cabal.project` and `scripts/nix/sha256map.nix`. -A link prepared by `prepareConnectionLink` is available to the chat before its link data is signed: a contact link with its short link, an invitation as its full link and its link key; one network request is made per creation. +A link is available to the chat before its link data is signed: a contact link with its short link from `prepareConnectionLink`, an invitation as its full link with its link key from `prepareInvitationLink`. One network request is made per creation. -**New links.** The contact link with its short link, or the invitation with its link key `plpLinkKey`, is returned by `prepareConnectionLink` before the user data is signed; the short link of an invitation, with the link id assigned by the server, is returned by `createConnectionForLink`: +**New links.** The short link of an invitation, with the link id assigned by the server, is returned by `createInvitationForLink`: ```haskell -prepareConnectionLink :: AgentClient -> UserId -> SConnectionMode c -> C.KeyPairEd25519 -> Maybe ByteString -> Bool -> Maybe CRClientData -> CR.InitialKeys -> UseRatchetKeys -> Maybe SMPServerWithAuth -> AE (CreatedConnLink c, PreparedLinkParams c) +prepareConnectionLink :: AgentClient -> UserId -> C.KeyPairEd25519 -> ByteString -> Bool -> Maybe CRClientData -> CR.InitialKeys -> UseRatchetKeys -> Maybe SMPServerWithAuth -> AE (CreatedConnLink 'CMContact, PreparedLinkParams (Maybe (RatchetKeyId, CR.RcvE2EPrivRatchetParams 'C.X448))) -createConnectionForLink :: AgentClient -> NetworkRequestMode -> UserId -> Bool -> CreatedConnLink c -> PreparedLinkParams c -> UserConnLinkData c -> SubscriptionMode -> AE (ConnId, CreatedConnLink c) +createConnectionForLink :: AgentClient -> NetworkRequestMode -> UserId -> Bool -> CreatedConnLink 'CMContact -> PreparedLinkParams (Maybe (RatchetKeyId, CR.RcvE2EPrivRatchetParams 'C.X448)) -> UserConnLinkData 'CMContact -> SubscriptionMode -> AE ConnId + +prepareInvitationLink :: AgentClient -> UserId -> CR.InitialKeys -> Maybe SMPServerWithAuth -> AE (ConnectionRequestUri 'CMInvitation, PreparedLinkParams (CR.RcvE2EPrivRatchetParams 'C.X448)) + +createInvitationForLink :: AgentClient -> NetworkRequestMode -> UserId -> Bool -> ConnectionRequestUri 'CMInvitation -> PreparedLinkParams (CR.RcvE2EPrivRatchetParams 'C.X448) -> UserConnLinkData 'CMInvitation -> SubscriptionMode -> AE (ConnId, CreatedConnLink 'CMInvitation) ``` -- The link entity id is optional; `Nothing` for an invitation. -- `PreparedLinkParams` is indexed by the connection mode. The ratchet keys are one field, `plpRatchetKeys :: PreparedRatchetKeys m`, in place of `plpAddressKeys`: +- The contact functions keep the signatures of master, with the type parameter of `PreparedLinkParams`. +- `PreparedLinkParams` is parameterised by the keys stored at creation; `plpAddressKeys` is renamed `plpRatchetKeys`: ```haskell - data PreparedRatchetKeys (m :: ConnectionMode) where - PRKInvitation :: RcvE2EPrivRatchetParams 'C.X448 -> PreparedRatchetKeys 'CMInvitation - PRKContact :: Maybe (RatchetKeyId, RcvE2EPrivRatchetParams 'C.X448) -> PreparedRatchetKeys 'CMContact + data PreparedLinkParams k = PreparedLinkParams + { plpNonce :: C.CbNonce, + plpQueueE2EKeys :: C.KeyPairX25519, + plpLinkKey :: LinkKey, + plpRootPrivKey :: C.PrivateKeyEd25519, + plpSignedFixedData :: ByteString, + plpSrvWithAuth :: SMPServerWithAuth, + plpInitKeys :: InitialKeys, + plpRatchetKeys :: k + } ``` -- Client notices are checked for contact mode only. -- Contact mode: the link given to `createConnectionForLink` is returned. -- Invitation mode, prepare: - - x3dh keys from `CR.generateRcvE2EParams`, PQ support from `CR.initialPQEncryption True pqInitKeys` - - `connReq = CRInvitationUri crData e2eRcvParams`, queue mode `QMMessaging` - - the returned link is `CCLink connReq Nothing`; the key is `plpLinkKey` - - `useDR` is ignored -- Invitation mode, create: - - link data: `SL.encodeSignUserData SCMInvitation`, encrypted by `encryptInvLinkData` with `SL.invShortLinkKdf plpLinkKey`; `newRcvConnSrv` uses the same function - - queue request: `CQRMessaging (Just CQRData {linkKey, privSigKey, srvReq = (sndId, srvData)})` -- In both modes the link data is encrypted before the connection is created, and the queue is created by the local `createLinkQueue`: - - the ratchet keys are stored: `PRKContact` keys by `storeAddressRatchetKeys`, `PRKInvitation` keys by `createRatchetX3dhKeys` - - `createRcvQueue` - - the connection is deleted when either step fails - - the returned link from `connReqWithShortLink`, moved from `newRcvConnSrv` to top level, with the created `RcvQueue` as a parameter — `CSLInvitation` with the link id from the server, PQ keys removed from the full link for `IKPQOn` + - contact link: `k = Maybe (RatchetKeyId, RcvE2EPrivRatchetParams 'C.X448)`, the address ratchet keys + - invitation: `k = RcvE2EPrivRatchetParams 'C.X448`, the X3DH keys +- Prepare, `prepareLinkParams :: ConnectionModeI c => C.KeyPairEd25519 -> Maybe ByteString -> Maybe CRClientData -> CR.InitialKeys -> SMPServerWithAuth -> QueueMode -> (ConnReqUriData -> ConnectionRequestUri c) -> k -> AM (ConnectionRequestUri c, PreparedLinkParams k)`: + - the nonce and the queue keys are generated; + - the connection request is built by the given function from the queue of the given mode; + - its fixed data is signed with the root key and the entity id. +- `prepareConnectionLink'`: the server is chosen; client notices are checked when `checkNotices` is set; the address ratchet keys are generated when `useDR` is set; `QMContact`. +- `prepareInvitationLink'`: + - the server is chosen and the root key pair is generated; + - X3DH keys from `CR.generateRcvE2EParams`, PQ support from `CR.initialPQEncryption True pqInitKeys`; + - `CRInvitationUri` with the X3DH parameters, `QMMessaging`; + - the entity id and the client data are `Nothing`. +- Create, `createLinkQueue :: AgentClient -> NetworkRequestMode -> UserId -> Bool -> SConnectionMode c -> PreparedLinkParams k -> SubscriptionMode -> ClntQueueReqData -> (ConnId -> AM ()) -> AM (ConnId, RcvQueue, SMPQueueUri)`: + - the connection is created; + - the keys are stored by the given action; + - the queue is created by `createRcvQueue`; + - the connection is deleted when either step fails. +- `createConnectionForLink'`: + - the link data is encrypted by `encryptContactLinkData` before the connection is created; + - the address ratchet keys are stored by `storeAddressRatchetKeys`; + - the sender id is compared after `NEW`, as on master. +- `createInvitationForLink'`: + - link data: `SL.encodeSignUserData SCMInvitation`, encrypted with `SL.invShortLinkKdf plpLinkKey`; + - queue request: `CQRMessaging (Just CQRData {linkKey, privSigKey, srvReq = (sndId, srvData)})`; + - the X3DH keys are stored by `createRatchetX3dhKeys`; + - the returned link from `connReqWithShortLink`: `CSLInvitation` with the link id from the server, PQ keys removed from the full link for `IKPQOn`. - Tests: - - a connection via an invitation made by prepare and create; its link data is read by the joining party; `plpLinkKey` equals the key in the returned `CSLInvitation` - - a contact link returned by `createConnectionForLink` equals the prepared link - - for link data above the size limit, `CMD LARGE` is returned in both modes and no connection is stored - - the six existing test calls are updated for the mode and the pair + - a connection via an invitation made by prepare and create: its link data is read by the joining party and holds the prepared invitation; `plpLinkKey` equals the key in the returned `CSLInvitation`; for `IKPQOn` the returned full link differs from the prepared invitation + - for link data above the size limit, `CMD LARGE` is returned for a contact link and for an invitation, and no connection is stored **Existing connections.** The link of a connection is returned without a network call: @@ -399,7 +419,6 @@ prepareConnShortLink :: AgentClient -> ConnId -> Maybe CRClientData -> AE (ConnS - `NEW` and `IDS` (`Protocol.hs`): the encodings and parsers for versions below 15, with `qReq` and `qm`. - `mkShortLinkCreds` (`Agent/Client.hs`): link data without a link id in the response is an error; the `THandleParams` parameter is removed. - `connReqWithShortLink`: absent short link credentials are an `INTERNAL` error. -- `createConnectionForLink'`: the sender id check of master, error `sender ID mismatch`, is removed; the sender id is compared by `mkShortLinkCreds` and by `connReqWithShortLink`. - `Transport.hs`: `shortLinksSMPVersion` is renamed `_shortLinksSMPVersion` and is not exported; `_proxyServerHandshakeSMPVersion` is removed. - Tests: `testInvitationShortLinkPrev` and `testProxyMatrixWithPrev` are removed. @@ -524,7 +543,7 @@ ALTER TABLE connections ADD COLUMN pres_header BLOB; **10. Link data** -- Invitation links (`APIAddContact`, `recreateConn`): the chat generates the root key pair and calls `prepareConnectionLink SCMInvitation`; the badge is presented with `PHLink` of `plpLinkKey`; then `createConnectionForLink` is called. +- Invitation links (`APIAddContact`, `recreateConn`): the chat calls `prepareInvitationLink`; the badge is presented with `PHLink` of `plpLinkKey`; then `createInvitationForLink` is called with the invitation and the params. - Invitation updates (`updatePCCShortLinkData`, `APISetConnectionIncognito`): `linkPresHeader` of the stored invitation short link. - Address (`APICreateMyAddress`): `linkPresHeader` of the key in the prepared `CSLContact`. - Address updates (`setMyAddressData`): the key of the stored short link. For an address without a short link, the key is taken from `prepareConnShortLink` when a badge is presented, then `setConnShortLink` is called once. diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 2e0ecfead3..43573e82cf 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."ee66dd68d4429e769da7f2506a48af5887e0f7af" = "0k58ayni6d36fjcahj8lvwkmg6yabmv60g8va6hh9z2ph09lhj2i"; + "https://github.com/simplex-chat/simplexmq.git"."a2f985dc68f870c0327fc3e871f2816828bcba2d" = "12a66936nliggq8k2pmk8x2lilrhvlg15rsmcv556s8gpzxjavdw"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."2330df3dc8c4b660674d3c02d76bb86f66c2abee" = "11pfj1vqsjdl26nplkr2l6z1xj7cn153qijj5gknmlzq3krn7x1i"; "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 f1b2ad3f7a..1c0510c31d 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -2124,12 +2124,11 @@ processChatCommand cxt nm = \case -- [incognito] generate profile for connection incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing subMode <- chatReadVar subscriptionMode - rootKey <- atomically . C.generateKeyPair =<< asks random - (preparedLink, preparedParams@PreparedLinkParams {plpLinkKey = LinkKey linkKey}) <- withAgent $ \a -> prepareConnectionLink a (aUserId user) SCMInvitation rootKey Nothing False Nothing IKUsePQ False Nothing + (preparedReq, preparedParams@PreparedLinkParams {plpLinkKey = LinkKey linkKey}) <- withAgent $ \a -> prepareInvitationLink a (aUserId user) IKUsePQ Nothing linkProfile <- presentUserBadge user incognitoProfile (Just $ PHLink linkKey) $ userProfileDirect user incognitoProfile Nothing True let userData = contactShortLinkData linkProfile {contactDomain = Nothing} Nothing userLinkData = UserInvLinkData userData - (connId, ccLink) <- withAgent $ \a -> createConnectionForLink a nm (aUserId user) True preparedLink preparedParams userLinkData subMode + (connId, ccLink) <- withAgent $ \a -> createInvitationForLink a nm (aUserId user) True preparedReq preparedParams userLinkData subMode ccLink' <- shortenCreatedLink ccLink -- TODO PQ pass minVersion from the current range conn <- withFastStore' $ \db -> createDirectConnection db user connId ccLink' Nothing ConnNew incognitoProfile subMode initialChatVersion PQSupportOn @@ -2170,10 +2169,9 @@ processChatCommand cxt nm = \case (agConnId, ccLink) <- if short then do - rootKey <- atomically . C.generateKeyPair =<< asks random - (preparedLink, preparedParams@PreparedLinkParams {plpLinkKey = LinkKey linkKey}) <- withAgent $ \a -> prepareConnectionLink a (aUserId newUser) SCMInvitation rootKey Nothing False Nothing IKPQOn False Nothing + (preparedReq, preparedParams@PreparedLinkParams {plpLinkKey = LinkKey linkKey}) <- withAgent $ \a -> prepareInvitationLink a (aUserId newUser) IKPQOn Nothing userLinkData <- UserInvLinkData . (`contactShortLinkData` Nothing) <$> presentUserBadge newUser Nothing (Just $ PHLink linkKey) (userProfileDirect newUser Nothing Nothing True) - withAgent $ \a -> createConnectionForLink a nm (aUserId newUser) True preparedLink preparedParams userLinkData subMode + withAgent $ \a -> createInvitationForLink a nm (aUserId newUser) True preparedReq preparedParams userLinkData subMode else withAgent $ \a -> createConnection a nm (aUserId newUser) True False SCMInvitation Nothing Nothing IKPQOn True subMode ccLink' <- shortenCreatedLink ccLink conn' <- withFastStore' $ \db -> do @@ -2472,7 +2470,7 @@ processChatCommand cxt nm = \case Just True -> (IKUsePQ, True) Just False -> (IKPQOn, True) Nothing -> (IKPQOn, False) - (ccLink, preparedParams) <- withAgent $ \a -> prepareConnectionLink a (aUserId user) SCMContact rootKey (Just entityId) True Nothing pqInitKeys useDR server_ + (ccLink, preparedParams) <- withAgent $ \a -> prepareConnectionLink a (aUserId user) rootKey entityId True Nothing pqInitKeys useDR server_ ccLink' <- shortenCreatedLink ccLink -- TODO [relays] relay: add identity, key to link data? userData <- @@ -2480,7 +2478,7 @@ processChatCommand cxt nm = \case then pure $ relayShortLinkData (userProfileDirect user Nothing Nothing True) else (`contactShortLinkData` Nothing) <$> presentUserBadge user Nothing (linkPresHeader <$> connShortLink' ccLink) (userProfileDirect user Nothing Nothing True) let userLinkData = UserContactLinkData UserContactData {direct = True, owners = [], relays = [], userData, ratchetKeys = Nothing} - (connId, _) <- withAgent $ \a -> createConnectionForLink a nm (aUserId user) True ccLink preparedParams userLinkData subMode + connId <- withAgent $ \a -> createConnectionForLink a nm (aUserId user) True ccLink preparedParams userLinkData subMode let ccLink'' = if isTrue userChatRelay then setShortLinkType CCTRelay ccLink' else ccLink' withFastStore $ \db -> createUserContactLink db user connId ccLink'' subMode rootPrivKey pure $ CRUserContactLinkCreated user ccLink'' @@ -2749,7 +2747,7 @@ processChatCommand cxt nm = \case 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) SCMContact rootKey (Just entityId) True (Just crClientData) IKPQOff False Nothing + (ccLink, preparedParams) <- withAgent $ \a -> prepareConnectionLink a (aUserId user) rootKey entityId True (Just crClientData) IKPQOff False Nothing ccLink' <- setShortLinkType CCTChannel <$> shortenCreatedLink ccLink sLnk <- case connShortLink' ccLink' of Just sl -> pure sl @@ -2762,7 +2760,7 @@ processChatCommand cxt nm = \case userData = encodeShortLinkData $ GroupShortLinkData {groupProfile = groupProfile', publicGroupData = Just (PublicGroupData 1)} userLinkData = UserContactLinkData UserContactData {direct = False, owners = [ownerAuth], relays = [], userData, ratchetKeys = Nothing} -- create connection with prepared link (single network call) - (connId, _) <- withAgent $ \a -> createConnectionForLink a nm (aUserId user) True ccLink preparedParams userLinkData subMode + connId <- withAgent $ \a -> createConnectionForLink a nm (aUserId user) True ccLink preparedParams userLinkData subMode let groupKeys = GKPublicGroup {groupRootKey = GRKPrivate rootPrivKey, memberPrivKey} setupLink gInfo = do -- TODO [relays] starting role should be communicated in protocol from owner to relays diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 3631331f75..a07a8c754b 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -4586,7 +4586,7 @@ runRelayRequestWorker a Worker {doWork} = do subMode <- chatReadVar subscriptionMode let crClientData = encodeJSON $ CRDataGroup groupLinkId -- prepare link with relayMemId as linkEntityId (no server request) - (ccLink, preparedParams) <- withAgent $ \a' -> prepareConnectionLink a' (aUserId user) SCMContact sigKeys (Just relayMemId) True (Just crClientData) CR.IKPQOff False Nothing + (ccLink, preparedParams) <- withAgent $ \a' -> prepareConnectionLink a' (aUserId user) sigKeys relayMemId True (Just crClientData) CR.IKPQOff False Nothing ccLink' <- setShortLinkType CCTGroup <$> shortenCreatedLink ccLink sLnk <- case connShortLink' ccLink' of Just sl -> pure sl @@ -4594,7 +4594,7 @@ runRelayRequestWorker a Worker {doWork} = do let userData = encodeShortLinkData $ RelayShortLinkData {relayProfile = fromLocalProfile p} userLinkData = UserContactLinkData UserContactData {direct = True, owners = [], relays = [], userData, ratchetKeys = Nothing} -- create connection with prepared link (single network call) - (connId, _) <- withAgent $ \a' -> createConnectionForLink a' NRMBackground (aUserId user) True ccLink preparedParams userLinkData subMode + connId <- withAgent $ \a' -> createConnectionForLink a' NRMBackground (aUserId user) True ccLink preparedParams userLinkData subMode -- TODO [relays] starting role should be communicated in protocol from owner to relays subRole <- asks $ channelSubscriberRole . config void $ withFastStore $ \db -> createGroupLink db gVar user gi connId ccLink' groupLinkId subRole subMode