From a2f985dc68f870c0327fc3e871f2816828bcba2d Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sat, 10 Oct 2026 23:15:19 +0100 Subject: [PATCH] agent: prepare invitation link, create ratchet when connection is prepared (#1880) * agent: prepare invitation link * simplify * refactor * fix * change min SMP version to 15 * remove dead code for SMP version <= 15 * update * diff * create ratchet when connection is prepared * createRatchetIfNotExists * clean up, only check client notices for contacts, fix orphan record * simplify * refactor --------- Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com> --- src/Simplex/Messaging/Agent.hs | 175 +++++++++++++++------- src/Simplex/Messaging/Agent/Protocol.hs | 7 +- src/Simplex/Messaging/Crypto/ShortLink.hs | 8 +- tests/AgentTests/FunctionalAPITests.hs | 92 +++++++++++- 4 files changed, 219 insertions(+), 63 deletions(-) diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 4d6913411..8d4af8002 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -63,7 +63,10 @@ module Simplex.Messaging.Agent createConnection, prepareConnectionLink, createConnectionForLink, + prepareInvitationLink, + createInvitationForLink, setConnShortLink, + prepareConnShortLink, deleteConnShortLink, getConnShortLink, resolveSimplexName, @@ -439,22 +442,39 @@ createConnection c nm userId enableNtfs checkNotices = withAgentEnv c .::: newCo -- Caller provides root signing key pair and link entity ID. -- Returns the created link and internal params. -- The link address is fully determined at this point. -prepareConnectionLink :: AgentClient -> UserId -> C.KeyPairEd25519 -> ByteString -> Bool -> Maybe CRClientData -> CR.InitialKeys -> UseRatchetKeys -> Maybe SMPServerWithAuth -> AE (CreatedConnLink 'CMContact, PreparedLinkParams) +prepareConnectionLink :: AgentClient -> UserId -> C.KeyPairEd25519 -> ByteString -> Bool -> Maybe CRClientData -> CR.InitialKeys -> UseRatchetKeys -> Maybe SMPServerWithAuth -> AE (CreatedConnLink 'CMContact, PreparedLinkParams (Maybe (RatchetKeyId, CR.RcvE2EPrivRatchetParams 'C.X448))) prepareConnectionLink c userId rootKey linkEntityId checkNotices clientData pqInitKeys useDR srv_ = withAgentEnv c $ prepareConnectionLink' c userId rootKey linkEntityId checkNotices clientData pqInitKeys useDR srv_ {-# INLINE prepareConnectionLink #-} -- | Create connection for prepared link (single network call). -- Validates that server response matches the prepared link. -createConnectionForLink :: AgentClient -> NetworkRequestMode -> UserId -> Bool -> CreatedConnLink 'CMContact -> PreparedLinkParams -> UserConnLinkData 'CMContact -> SubscriptionMode -> AE ConnId +createConnectionForLink :: AgentClient -> NetworkRequestMode -> UserId -> Bool -> CreatedConnLink 'CMContact -> PreparedLinkParams (Maybe (RatchetKeyId, CR.RcvE2EPrivRatchetParams 'C.X448)) -> UserConnLinkData 'CMContact -> SubscriptionMode -> AE ConnId createConnectionForLink c nm userId enableNtfs = withAgentEnv c .:: createConnectionForLink' c nm userId enableNtfs {-# INLINE createConnectionForLink #-} +-- | Prepare one-time invitation link (no network call). +-- Returns the invitation and internal params with the link key. +-- The link ID is assigned by the server when the connection is created. +prepareInvitationLink :: AgentClient -> UserId -> CR.InitialKeys -> Maybe SMPServerWithAuth -> AE (ConnectionRequestUri 'CMInvitation, PreparedLinkParams (CR.RcvE2EPrivRatchetParams 'C.X448)) +prepareInvitationLink c userId pqInitKeys srv_ = withAgentEnv c $ prepareInvitationLink' c userId pqInitKeys srv_ +{-# INLINE prepareInvitationLink #-} + +-- | Create connection for prepared invitation link (single network call). +-- Returns the created link with the link ID assigned by the server. +createInvitationForLink :: AgentClient -> NetworkRequestMode -> UserId -> Bool -> ConnectionRequestUri 'CMInvitation -> PreparedLinkParams (CR.RcvE2EPrivRatchetParams 'C.X448) -> UserConnLinkData 'CMInvitation -> SubscriptionMode -> AE (ConnId, CreatedConnLink 'CMInvitation) +createInvitationForLink c nm userId enableNtfs = withAgentEnv c .:: createInvitationForLink' c nm userId enableNtfs +{-# INLINE createInvitationForLink #-} + -- | Create or update user's contact connection short link setConnShortLink :: AgentClient -> NetworkRequestMode -> ConnId -> SConnectionMode c -> UserConnLinkData c -> Maybe CRClientData -> NewRatchetKeys -> Maybe CR.InitialKeys -> AE (ConnShortLink c) setConnShortLink c = withAgentEnv c .:::. setConnShortLink' c {-# INLINE setConnShortLink #-} +prepareConnShortLink :: AgentClient -> ConnId -> Maybe CRClientData -> AE (ConnShortLink 'CMContact) +prepareConnShortLink c = withAgentEnv c .: prepareConnShortLink' c +{-# INLINE prepareConnShortLink #-} + deleteConnShortLink :: AgentClient -> NetworkRequestMode -> ConnId -> SConnectionMode c -> AE () deleteConnShortLink c = withAgentEnv c .:. deleteConnShortLink' c {-# INLINE deleteConnShortLink #-} @@ -1035,30 +1055,20 @@ newConn c nm userId enableNtfs checkNotices cMode linkData_ clientData pqInitKey -- | Prepare connection link for contact mode (no network, no database). -- Caller provides root signing key pair and link entity ID. -prepareConnectionLink' :: AgentClient -> UserId -> C.KeyPairEd25519 -> ByteString -> Bool -> Maybe CRClientData -> CR.InitialKeys -> UseRatchetKeys -> Maybe SMPServerWithAuth -> AM (CreatedConnLink 'CMContact, PreparedLinkParams) -prepareConnectionLink' c userId rootKey@(_, plpRootPrivKey) linkEntityId checkNotices clientData pqInitKeys useDR srv_ = do - g <- asks random - plpSrvWithAuth@(ProtoServerWithAuth srv _) <- maybe (getSMPServer c userId) pure srv_ - when checkNotices $ checkClientNotices c plpSrvWithAuth - AgentConfig {smpClientVRange, smpAgentVRange} <- asks config - plpNonce@(C.CbNonce corrId) <- atomically $ C.randomCbNonce g - plpQueueE2EKeys@(e2ePubKey, _) <- atomically $ C.generateKeyPair g +prepareConnectionLink' :: AgentClient -> UserId -> C.KeyPairEd25519 -> ByteString -> Bool -> Maybe CRClientData -> CR.InitialKeys -> UseRatchetKeys -> Maybe SMPServerWithAuth -> AM (CreatedConnLink 'CMContact, PreparedLinkParams (Maybe (RatchetKeyId, CR.RcvE2EPrivRatchetParams 'C.X448))) +prepareConnectionLink' c userId rootKey linkEntityId checkNotices clientData pqInitKeys useDR srv_ = do + srvWithAuth@(ProtoServerWithAuth srv _) <- maybe (getSMPServer c userId) pure srv_ + when checkNotices $ checkClientNotices c srvWithAuth addrKeys_ <- if useDR then Just <$> generateAddressRatchetKeys pqInitKeys else pure Nothing - let sndId = SMP.EntityId $ B.take 24 $ C.sha3_384 corrId - qUri = SMPQueueUri smpClientVRange $ SMPQueueAddress srv sndId e2ePubKey (Just QMContact) - connReq = CRContactUri (ConnReqUriData SSSimplex smpAgentVRange [qUri] clientData) (fst <$> addrKeys_) - (plpLinkKey, plpSignedFixedData) = SL.encodeSignFixedData rootKey smpAgentVRange connReq (Just linkEntityId) - ccLink = CCLink connReq $ Just $ CSLContact SLSServer CCTContact srv plpLinkKey - params = PreparedLinkParams {plpNonce, plpQueueE2EKeys, plpLinkKey, plpRootPrivKey, plpSignedFixedData, plpSrvWithAuth, plpInitKeys = pqInitKeys, plpAddressKeys = snd <$> addrKeys_} - pure (ccLink, params) + (connReq, params@PreparedLinkParams {plpLinkKey}) <- + prepareLinkParams rootKey (Just linkEntityId) clientData pqInitKeys srvWithAuth QMContact (`CRContactUri` (fst <$> addrKeys_)) (snd <$> addrKeys_) + pure (CCLink connReq $ Just $ CSLContact SLSServer CCTContact srv plpLinkKey, params) -- | Create connection for prepared link (single network call). -createConnectionForLink' :: AgentClient -> NetworkRequestMode -> UserId -> Bool -> CreatedConnLink 'CMContact -> PreparedLinkParams -> UserConnLinkData 'CMContact -> SubscriptionMode -> AM ConnId -createConnectionForLink' c nm userId enableNtfs (CCLink connReq _) PreparedLinkParams {plpNonce, plpQueueE2EKeys, plpLinkKey, plpRootPrivKey, plpSignedFixedData, plpSrvWithAuth, plpInitKeys, plpAddressKeys} userLinkData subMode = do +createConnectionForLink' :: AgentClient -> NetworkRequestMode -> UserId -> Bool -> CreatedConnLink 'CMContact -> PreparedLinkParams (Maybe (RatchetKeyId, CR.RcvE2EPrivRatchetParams 'C.X448)) -> UserConnLinkData 'CMContact -> SubscriptionMode -> AM ConnId +createConnectionForLink' c nm userId enableNtfs (CCLink connReq _) params@PreparedLinkParams {plpLinkKey, plpRootPrivKey, plpSignedFixedData, plpRatchetKeys} userLinkData subMode = do g <- asks random AgentConfig {smpAgentVRange} <- asks config - connId <- newConnNoQueues c userId enableNtfs SCMContact (CR.connPQEncryption plpInitKeys) - mapM_ (storeAddressRatchetKeys c connId) plpAddressKeys let CRContactUri ConnReqUriData {crSmpQueues = SMPQueueUri _ SMPQueueAddress {senderId = sndId} :| _} addrKeys_ = connReq userLinkData' = case addrKeys_ of Just arKeys -> let UserContactLinkData ucd = userLinkData in UserContactLinkData ucd {ratchetKeys = Just arKeys} @@ -1066,13 +1076,54 @@ createConnectionForLink' c nm userId enableNtfs (CCLink connReq _) PreparedLinkP md = SL.encodeSignUserData SCMContact plpRootPrivKey smpAgentVRange userLinkData' linkData = (plpSignedFixedData, md) qd <- encryptContactLinkData g plpRootPrivKey plpLinkKey sndId linkData - (_, qUri) <- - createRcvQueue c nm userId connId plpSrvWithAuth enableNtfs subMode (Just plpNonce) qd plpQueueE2EKeys - `catchE` \e -> withStore' c (`deleteConnRecord` connId) >> throwE e + (connId, _, qUri) <- createLinkQueue c nm userId enableNtfs SCMContact params subMode qd $ \connId -> mapM_ (storeAddressRatchetKeys c connId) plpRatchetKeys let SMPQueueUri _ SMPQueueAddress {senderId = actualSndId} = qUri unless (actualSndId == sndId) $ throwE $ INTERNAL "createConnectionForLink: sender ID mismatch" pure connId +-- | Prepare one-time invitation link (no network, no database). +prepareInvitationLink' :: AgentClient -> UserId -> CR.InitialKeys -> Maybe SMPServerWithAuth -> AM (ConnectionRequestUri 'CMInvitation, PreparedLinkParams (CR.RcvE2EPrivRatchetParams 'C.X448)) +prepareInvitationLink' c userId pqInitKeys srv_ = do + g <- asks random + srvWithAuth <- maybe (getSMPServer c userId) pure srv_ + e2eVR <- asks $ e2eEncryptVRange . config + rootKey <- atomically $ C.generateKeyPair g + (pks, e2eRcvParams) <- liftIO $ CR.generateRcvE2EParams g (maxVersion e2eVR) (CR.initialPQEncryption True pqInitKeys) + prepareLinkParams rootKey Nothing Nothing pqInitKeys srvWithAuth QMMessaging (`CRInvitationUri` toVersionRangeT e2eRcvParams e2eVR) pks + +-- | Create connection for prepared invitation link (single network call). +createInvitationForLink' :: AgentClient -> NetworkRequestMode -> UserId -> Bool -> ConnectionRequestUri 'CMInvitation -> PreparedLinkParams (CR.RcvE2EPrivRatchetParams 'C.X448) -> UserConnLinkData 'CMInvitation -> SubscriptionMode -> AM (ConnId, CreatedConnLink 'CMInvitation) +createInvitationForLink' c nm userId enableNtfs connReq params@PreparedLinkParams {plpLinkKey, plpRootPrivKey, plpSignedFixedData, plpInitKeys, plpRatchetKeys} userLinkData subMode = do + g <- asks random + AgentConfig {smpAgentVRange} <- asks config + let CRInvitationUri ConnReqUriData {crSmpQueues = qUri@(SMPQueueUri _ SMPQueueAddress {senderId = sndId}) :| _} _ = connReq + md = SL.encodeSignUserData SCMInvitation plpRootPrivKey smpAgentVRange userLinkData + srvData <- liftError id $ SL.encryptLinkData g (SL.invShortLinkKdf plpLinkKey) (plpSignedFixedData, md) + let qd = CQRMessaging $ Just CQRData {linkKey = plpLinkKey, privSigKey = plpRootPrivKey, srvReq = (sndId, srvData)} + (connId, rq, qUri') <- createLinkQueue c nm userId enableNtfs SCMInvitation params subMode qd $ \connId -> withStore' c $ \db -> createRatchetX3dhKeys db connId plpRatchetKeys + (connId,) <$> connReqWithShortLink plpInitKeys qUri connReq qUri' rq + +-- | Generate queue keys and sign fixed link data (no network, no database). +prepareLinkParams :: ConnectionModeI c => C.KeyPairEd25519 -> Maybe ByteString -> Maybe CRClientData -> CR.InitialKeys -> SMPServerWithAuth -> QueueMode -> (ConnReqUriData -> ConnectionRequestUri c) -> k -> AM (ConnectionRequestUri c, PreparedLinkParams k) +prepareLinkParams rootKey@(_, plpRootPrivKey) linkEntityId clientData plpInitKeys plpSrvWithAuth@(ProtoServerWithAuth srv _) qm mkConnReq plpRatchetKeys = do + g <- asks random + AgentConfig {smpClientVRange, smpAgentVRange} <- asks config + plpNonce@(C.CbNonce corrId) <- atomically $ C.randomCbNonce g + plpQueueE2EKeys@(e2ePubKey, _) <- atomically $ C.generateKeyPair g + let sndId = SMP.EntityId $ B.take 24 $ C.sha3_384 corrId + qUri = SMPQueueUri smpClientVRange $ SMPQueueAddress srv sndId e2ePubKey (Just qm) + connReq = mkConnReq $ ConnReqUriData SSSimplex smpAgentVRange [qUri] clientData + (plpLinkKey, plpSignedFixedData) = SL.encodeSignFixedData rootKey smpAgentVRange connReq linkEntityId + pure (connReq, PreparedLinkParams {plpNonce, plpQueueE2EKeys, plpLinkKey, plpRootPrivKey, plpSignedFixedData, plpSrvWithAuth, plpInitKeys, plpRatchetKeys}) + +-- | Create connection and its queue with link data; the connection is deleted when either step fails. +createLinkQueue :: AgentClient -> NetworkRequestMode -> UserId -> Bool -> SConnectionMode c -> PreparedLinkParams k -> SubscriptionMode -> ClntQueueReqData -> (ConnId -> AM ()) -> AM (ConnId, RcvQueue, SMPQueueUri) +createLinkQueue c nm userId enableNtfs cMode PreparedLinkParams {plpNonce, plpQueueE2EKeys, plpSrvWithAuth, plpInitKeys} subMode qd storeKeys = do + connId <- newConnNoQueues c userId enableNtfs cMode (CR.connPQEncryption plpInitKeys) + uncurry (connId,,) + <$> (storeKeys connId >> createRcvQueue c nm userId connId plpSrvWithAuth enableNtfs subMode (Just plpNonce) qd plpQueueE2EKeys) + `catchE` \e -> withStore' c (`deleteConnRecord` connId) >> throwE e + generateAddressRatchetKeys :: CR.InitialKeys -> AM (AddressRatchetKeys, (RatchetKeyId, CR.RcvE2EPrivRatchetParams 'C.X448)) generateAddressRatchetKeys pqInitKeys = do g <- asks random @@ -1197,7 +1248,7 @@ setConnShortLink' c nm connId cMode userLinkData clientData rotateKeys pqKeys_ = prepareContactLinkData ConnData {} rq@RcvQueue {shortLink} (UserContactLinkData ucd) = do liftEitherWith (CMD PROHIBITED . ("setConnShortLink: " <>)) $ validateOwners shortLink ucd g <- asks random - AgentConfig {smpClientVRange = vr, smpAgentVRange} <- asks config + AgentConfig {smpAgentVRange} <- asks config -- rotate makes fresh keys from InitialKeys; otherwise keep current keys or create based on InitialKeys if there are no ratchet keys let currKeys = currentAddressRatchetKeys c connId ratchetKeys <- case pqKeys_ of @@ -1207,32 +1258,47 @@ setConnShortLink' c nm connId cMode userLinkData clientData rotateKeys pqKeys_ = Nothing -> currKeys let ud = UserContactLinkData ucd {ratchetKeys} cslContact = CSLContact SLSServer CCTContact (qServer rq) - case shortLink of - Just ShortLinkCreds {shortLinkId, shortLinkKey, linkPrivSigKey, linkEncFixedData} -> do + (ShortLinkCreds {shortLinkId, shortLinkKey, linkPrivSigKey, linkEncFixedData}, k) <- case shortLink of + Just creds@ShortLinkCreds {shortLinkId, shortLinkKey} -> do let (linkId, k) = SL.contactShortLinkKdf shortLinkKey unless (shortLinkId == linkId) $ throwE $ INTERNAL "setConnShortLink: link ID is not derived from link" - d <- liftError id $ SL.encryptUserData g k $ SL.encodeSignUserData SCMContact linkPrivSigKey smpAgentVRange ud - pure (rq, linkId, cslContact shortLinkKey, (linkEncFixedData, d)) - Nothing -> do - sigKeys@(_, privSigKey) <- atomically $ C.generateKeyPair @'C.Ed25519 g - let qUri = SMPQueueUri vr $ (rcvSMPQueueAddress rq) {queueMode = Just QMContact} - connReq = CRContactUri (ConnReqUriData SSSimplex smpAgentVRange [qUri] clientData) ratchetKeys - (linkKey, linkData) = SL.encodeSignLinkData sigKeys smpAgentVRange connReq Nothing ud - (linkId, k) = SL.contactShortLinkKdf linkKey - srvData <- liftError id $ SL.encryptLinkData g k linkData - let slCreds = ShortLinkCreds linkId linkKey privSigKey Nothing (fst srvData) - withStore' c $ \db -> updateShortLinkCreds db rq slCreds - pure (rq, linkId, cslContact linkKey, srvData) - prepareInvLinkData :: RcvQueue -> UserConnLinkData 'CMInvitation -> AM (RcvQueue, SMP.LinkId, ConnShortLink 'CMInvitation, QueueLinkData) - prepareInvLinkData rq@RcvQueue {shortLink} ud = case shortLink of - Just ShortLinkCreds {shortLinkId, shortLinkKey, linkPrivSigKey, linkEncFixedData} -> do - g <- asks random - AgentConfig {smpAgentVRange} <- asks config - let k = SL.invShortLinkKdf shortLinkKey - d <- liftError id $ SL.encryptUserData g k $ SL.encodeSignUserData SCMInvitation linkPrivSigKey smpAgentVRange ud - let sl = CSLInvitation SLSServer (qServer rq) shortLinkId shortLinkKey - pure (rq, shortLinkId, sl, (linkEncFixedData, d)) - Nothing -> throwE $ CMD PROHIBITED "setConnShortLink: no ShortLinkCreds in invitation" + pure (creds, k) + Nothing -> newContactLinkCreds c rq clientData + d <- liftError id $ SL.encryptUserData g k $ SL.encodeSignUserData SCMContact linkPrivSigKey smpAgentVRange ud + pure (rq, shortLinkId, cslContact shortLinkKey, (linkEncFixedData, d)) + +newContactLinkCreds :: AgentClient -> RcvQueue -> Maybe CRClientData -> AM (ShortLinkCreds, C.SbKey) +newContactLinkCreds c rq clientData = do + g <- asks random + AgentConfig {smpClientVRange = vr, smpAgentVRange} <- asks config + sigKeys@(_, privSigKey) <- atomically $ C.generateKeyPair @'C.Ed25519 g + let qUri = SMPQueueUri vr $ (rcvSMPQueueAddress rq) {queueMode = Just QMContact} + connReq = CRContactUri (ConnReqUriData SSSimplex smpAgentVRange [qUri] clientData) Nothing + (linkKey, fixedData) = SL.encodeSignFixedData sigKeys smpAgentVRange connReq Nothing + (linkId, k) = SL.contactShortLinkKdf linkKey + encFixedData <- liftError id $ SL.encryptFixedData g k fixedData + let slCreds = ShortLinkCreds linkId linkKey privSigKey Nothing encFixedData + withStore' c $ \db -> updateShortLinkCreds db rq slCreds + pure (slCreds, k) + +prepareInvLinkData :: RcvQueue -> UserConnLinkData 'CMInvitation -> AM (RcvQueue, SMP.LinkId, ConnShortLink 'CMInvitation, QueueLinkData) +prepareInvLinkData rq@RcvQueue {shortLink} ud = case shortLink of + Just ShortLinkCreds {shortLinkId, shortLinkKey, linkPrivSigKey, linkEncFixedData} -> do + g <- asks random + AgentConfig {smpAgentVRange} <- asks config + let k = SL.invShortLinkKdf shortLinkKey + d <- liftError id $ SL.encryptUserData g k $ SL.encodeSignUserData SCMInvitation linkPrivSigKey smpAgentVRange ud + let sl = CSLInvitation SLSServer (qServer rq) shortLinkId shortLinkKey + pure (rq, shortLinkId, sl, (linkEncFixedData, d)) + Nothing -> throwE $ CMD PROHIBITED "setConnShortLink: no ShortLinkCreds in invitation" + +prepareConnShortLink' :: AgentClient -> ConnId -> Maybe CRClientData -> AM (ConnShortLink 'CMContact) +prepareConnShortLink' c connId clientData = + withConnLock c connId "prepareConnShortLink" $ + withStore c (`getConn` connId) >>= \case + SomeConn _ (ContactConnection _ rq@RcvQueue {shortLink}) -> + CSLContact SLSServer CCTContact (qServer rq) . shortLinkKey <$> maybe (fst <$> newContactLinkCreds c rq clientData) pure shortLink + _ -> throwE $ CMD PROHIBITED "prepareConnShortLink: not contact address" deleteConnShortLink' :: AgentClient -> NetworkRequestMode -> ConnId -> SConnectionMode c -> AM () deleteConnShortLink' c nm connId cMode = @@ -1443,11 +1509,11 @@ newConnToAccept c userId connId enableNtfs invId pqSup = do pure (connId', CRBRatchet $ ratchetVerifyCodes ratchetState) newConnToAcceptDR :: AgentClient -> UserId -> ConnId -> DRInvitation -> Bool -> AM ConnData -newConnToAcceptDR c userId connId DRInvitation {agentVersion, pqSupport} enableNtfs = do +newConnToAcceptDR c userId connId DRInvitation {ratchetState, agentVersion, pqSupport} enableNtfs = do g <- asks random let cData = ConnData {userId, connId, connAgentVersion = agentVersion, enableNtfs, lastExternalSndId = 0, deleted = False, ratchetSyncState = RSOk, pqSupport, serviceRequestExpiresAt = Nothing} - connId' <- withStore c $ \db -> createNewConn db g cData SCMInvitation - pure (cData {connId = connId'} :: ConnData) + withStore c $ \db -> + createNewConn db g cData SCMInvitation $>>= \connId' -> Right (cData {connId = connId'} :: ConnData) <$ createRatchet db connId' ratchetState joinConn :: AgentClient -> NetworkRequestMode -> UserId -> ConnId -> Bool -> ConnectionRequestUri c -> ConnInfo -> PQSupport -> SubscriptionMode -> AM SndQueueSecured joinConn c nm userId connId enableNtfs cReq cInfo pqSupport subMode = do @@ -1505,9 +1571,8 @@ startJoinInvitationDR c userId ConnData {connId} DRInvitation {ratchetState, rep qInfo <- maybe (throwE $ AGENT A_VERSION) pure $ replyQueue `proveCompatible` clientVRange (q, _) <- lift $ newSndQueue userId connId qInfo Nothing withStore c $ \db -> runExceptT $ do - liftIO $ lockConnForUpdate db connId - liftIO $ createRatchet db connId ratchetState - ExceptT $ updateNewConnSnd db connId q + sq <- ExceptT $ updateNewConnSnd db connId q + sq <$ liftIO (createRatchet db connId ratchetState) connRequestAgentVersion :: AgentClient -> ConnectionRequestUri c -> IO (Maybe VersionSMPA) connRequestAgentVersion c cReq = withAgentEnv' c $ case cReq of diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index 3ea9f3b1a..7625092d1 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -246,7 +246,6 @@ import Simplex.Messaging.Crypto.Ratchet RcvE2ERatchetParams, RcvE2ERatchetParamsUri, SndE2ERatchetParams, - RcvE2EPrivRatchetParams, pattern PQSupportOff, pattern PQSupportOn, ) @@ -1627,7 +1626,7 @@ newtype LinkKey = LinkKey ByteString -- sha3-256(fixed_data) instance ToField LinkKey where toField (LinkKey s) = toField $ Binary s -- | Parameters for creating a connection with a prepared link. -data PreparedLinkParams = PreparedLinkParams +data PreparedLinkParams k = PreparedLinkParams { -- | Correlation ID / determines sender ID plpNonce :: C.CbNonce, -- | Queue E2EE DH key pair @@ -1642,8 +1641,8 @@ data PreparedLinkParams = PreparedLinkParams plpSrvWithAuth :: SMPServerWithAuth, -- | Initial PQ keys plpInitKeys :: InitialKeys, - -- | Contact address double ratchet keys - plpAddressKeys :: Maybe (RatchetKeyId, RcvE2EPrivRatchetParams 'C.X448) + -- | Contact address double ratchet keys or invitation X3DH keys + plpRatchetKeys :: k } instance ConnectionModeI c => ToField (ConnectionLink c) where toField = toField . Binary . strEncode diff --git a/src/Simplex/Messaging/Crypto/ShortLink.hs b/src/Simplex/Messaging/Crypto/ShortLink.hs index 840366543..2ebd8d350 100644 --- a/src/Simplex/Messaging/Crypto/ShortLink.hs +++ b/src/Simplex/Messaging/Crypto/ShortLink.hs @@ -17,6 +17,7 @@ module Simplex.Messaging.Crypto.ShortLink encodeSignUserData, newOwnerAuth, encryptLinkData, + encryptFixedData, encryptUserData, decryptLinkData, ) @@ -84,9 +85,10 @@ newOwnerAuth g ownerId signingKey = do pure (ownerPrivKey, OwnerAuth {ownerId, ownerKey, authOwnerSig}) encryptLinkData :: TVar ChaChaDRG -> C.SbKey -> (ByteString, ByteString) -> ExceptT AgentErrorType IO QueueLinkData -encryptLinkData g k = bimapM (encrypt fixedDataPaddedLength) (encrypt userDataPaddedLength) - where - encrypt len = encryptData g k len +encryptLinkData g k = bimapM (encryptFixedData g k) (encryptUserData g k) + +encryptFixedData :: TVar ChaChaDRG -> C.SbKey -> ByteString -> ExceptT AgentErrorType IO EncDataBytes +encryptFixedData g k s = encryptData g k fixedDataPaddedLength s encryptUserData :: TVar ChaChaDRG -> C.SbKey -> ByteString -> ExceptT AgentErrorType IO EncDataBytes encryptUserData g k s = encryptData g k userDataPaddedLength s diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index bdc37f6e2..090e8b2f3 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -92,7 +92,7 @@ import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), Env (..), InitialAg import Simplex.Messaging.Agent.Protocol hiding (CON, CONF, INFO, REQ, SENT) import qualified Simplex.Messaging.Agent.Protocol as A import Simplex.Messaging.Agent.Store (Connection' (..), SomeConn' (..), StoredRcvQueue (..)) -import Simplex.Messaging.Agent.Store.AgentStore (deleteRatchetKeyHashesExpired, getConn, getRatchetX3dhKeys) +import Simplex.Messaging.Agent.Store.AgentStore (deleteRatchetKeyHashesExpired, getConn, getConnIds, getRatchetX3dhKeys) import Simplex.Messaging.Agent.Store.Common (DBStore (..), withTransaction) import Simplex.Messaging.Agent.Store.Interface import qualified Simplex.Messaging.Agent.Store.DB as DB @@ -371,6 +371,10 @@ functionalAPITests ps = do testAddDRViaSetConnShortLink ps it "should resume DR accept after a transient failure (reuse the send queue and ratchet)" $ testAcceptContactDRResumeAfterOffline ps + it "should store the ratchet when DR accept is prepared" $ + withSmpServer ps $ testAcceptContactDRPrepared False + it "should create the ratchet at DR accept when the prepared connection has none" $ + withSmpServer ps $ testAcceptContactDRPrepared True it "should support rejecting contact request" $ withSmpServer ps testRejectContactRequest it "should communicate rejection reason via double ratchet" $ @@ -421,6 +425,10 @@ functionalAPITests ps = do it "should connect via added contact short link after restart" $ testAddContactShortLinkRestart ps it "should create and get short links with the old contact queues" $ testOldContactQueueShortLink ps it "should connect via prepared connection link" $ testPrepareCreateConnectionLink ps + it "should connect via prepared invitation link" $ testPrepareCreateInvitationLink ps + it "should not create connection for prepared invitation link with too large data" $ testPrepareInvitationLinkTooLarge ps + it "should not create connection for prepared contact link with too large data" $ testPrepareContactLinkTooLarge ps + it "should connect via short link prepared for existing contact connection" $ testPrepareConnShortLink ps describe "Message delivery" $ do describe "update connection agent version on received messages" $ do it "should increase if compatible, shouldn'ps decrease" $ @@ -1241,6 +1249,30 @@ testAcceptContactDRResumeAfterOffline ps = withAgentClients2 $ \alice bob -> do ("", _, A.CONF confId _ _ "alice's connInfo") <- get bob allowConfirmGreet alice bobId bob aliceId confId addrIK pqEnc +testAcceptContactDRPrepared :: HasCallStack => Bool -> IO () +testAcceptContactDRPrepared ratchetDeleted = + withAgentClients2 $ \alice bob -> runRight_ $ do + let userLinkData = UserContactLinkData UserContactData {direct = True, owners = [], relays = [], userData = UserLinkData "test user data", ratchetKeys = Nothing} + pqEnc = PQEncryption $ pqConnectionMode IKPQOn PQSupportOn + (_, CCLink connReq _) <- A.createConnection alice NRMInteractive 1 True True SCMContact (Just userLinkData) Nothing IKPQOn True SMSubscribe + (aliceId, CRBRatchet codes) <- A.prepareConnectionToJoin bob 1 True connReq PQSupportOn + void $ A.joinConnection bob NRMInteractive 1 aliceId True connReq "bob's connInfo" PQSupportOn SMSubscribe + ("", _, A.REQ invId _ _ "bob's connInfo" (CRBRatchet reqCodes) _) <- get alice + (bobId, CRBRatchet acceptCodes) <- A.prepareConnectionToAccept alice 1 True invId PQSupportOn + liftIO $ do + reqCodes `shouldBe` codes + acceptCodes `shouldBe` codes + if ratchetDeleted + then do + liftIO $ withTransaction (store $ agentEnv alice) (`DB.execute_` "DELETE FROM ratchets") + Left (CONN NOT_FOUND _) <- tryError $ getConnectionVerifyCodes alice bobId + pure () + else getConnectionVerifyCodes alice bobId >>= liftIO . (`shouldBe` codes) + void $ acceptContact alice 1 bobId True invId "alice's connInfo" PQSupportOn SMSubscribe + ("", _, A.CONF confId _ _ "alice's connInfo") <- get bob + allowConfirmGreet alice bobId bob aliceId confId IKPQOn pqEnc + getConnectionVerifyCodes alice bobId >>= liftIO . (`shouldBe` codes) + runAgentClientContactTestPQ3 :: HasCallStack => Bool -> (AgentClient, InitialKeys) -> (AgentClient, PQSupport) -> (AgentClient, PQSupport) -> AgentMsgId -> IO () runAgentClientContactTestPQ3 viaProxy (alice, aPQ) (bob, bPQ) (tom, tPQ) baseId = runRight_ $ do (_, CCLink qInfo Nothing) <- A.createConnection alice NRMInteractive 1 True True SCMContact Nothing Nothing aPQ False SMSubscribe @@ -2055,6 +2087,64 @@ testPrepareCreateConnectionLink ps = withSmpServer ps $ withAgentClients2 $ \a b get b ##> ("", bId, CON) exchangeGreetings a aId b bId +testPrepareCreateInvitationLink :: HasCallStack => (ASrvTransport, AStoreType) -> IO () +testPrepareCreateInvitationLink ps = withSmpServer ps $ withAgentClients2 $ \a b -> do + let userData = UserLinkData "test user data" + runRight_ $ do + (preparedReq, preparedParams@PreparedLinkParams {plpLinkKey}) <- A.prepareInvitationLink a 1 CR.IKPQOn Nothing + (bId, CCLink connReq (Just shortLink@(CSLInvitation _ _ _ linkKey))) <- A.createInvitationForLink a NRMInteractive 1 True preparedReq preparedParams (UserInvLinkData userData) SMSubscribe + liftIO $ linkKey `shouldBe` plpLinkKey + (_, connData', connReq') <- getConnShortLink b 1 shortLink + liftIO $ connReq' `shouldBe` preparedReq + liftIO $ connReq `shouldNotBe` preparedReq + liftIO $ linkUserData connData' `shouldBe` userData + testJoinConn_ False True a bId b connReq + +testPrepareInvitationLinkTooLarge :: HasCallStack => (ASrvTransport, AStoreType) -> IO () +testPrepareInvitationLinkTooLarge ps = withSmpServer ps $ withAgent 1 agentCfg initAgentServers testDB $ \a -> do + runRight_ $ do + (preparedReq, preparedParams) <- A.prepareInvitationLink a 1 CR.IKPQOn Nothing + Left (A.CMD LARGE _) <- tryError $ A.createInvitationForLink a NRMInteractive 1 True preparedReq preparedParams (UserInvLinkData $ UserLinkData $ B.replicate 14000 'a') SMSubscribe + pure () + withTransaction (store $ agentEnv a) getConnIds `shouldReturn` [] + +testPrepareContactLinkTooLarge :: HasCallStack => (ASrvTransport, AStoreType) -> IO () +testPrepareContactLinkTooLarge ps = withSmpServer ps $ withAgent 1 agentCfg initAgentServers testDB $ \a -> do + let userCtData = UserContactData {direct = True, owners = [], relays = [], userData = UserLinkData $ B.replicate 14000 'a', ratchetKeys = Nothing} + g <- C.newRandom + rootKey <- atomically $ C.generateKeyPair g + linkEntId <- atomically $ C.randomBytes 32 g + runRight_ $ do + (ccLink, preparedParams) <- A.prepareConnectionLink a 1 rootKey linkEntId True Nothing CR.IKPQOn True Nothing + Left (A.CMD LARGE _) <- tryError $ A.createConnectionForLink a NRMInteractive 1 True ccLink preparedParams (UserContactLinkData userCtData) SMSubscribe + pure () + withTransaction (store $ agentEnv a) getConnIds `shouldReturn` [] + +testPrepareConnShortLink :: HasCallStack => (ASrvTransport, AStoreType) -> IO () +testPrepareConnShortLink ps = withSmpServer ps $ withAgentClients2 $ \a b -> do + let userCtData = UserContactData {direct = True, owners = [], relays = [], userData = UserLinkData "test user data", ratchetKeys = Nothing} + runRight_ $ do + (contactId, CCLink _ Nothing) <- A.createConnection a NRMInteractive 1 True True SCMContact Nothing Nothing CR.IKPQOn False SMSubscribe + shortLink <- A.prepareConnShortLink a contactId Nothing + shortLink' <- A.prepareConnShortLink a contactId Nothing + liftIO $ shortLink' `shouldBe` shortLink + shortLink'' <- A.setConnShortLink a NRMInteractive contactId SCMContact (UserContactLinkData userCtData) Nothing False Nothing + liftIO $ shortLink'' `shouldBe` shortLink + (_, ContactLinkData _ userCtData', connReq) <- getConnShortLink b 1 shortLink + liftIO $ userCtData' `shouldBe` userCtData + (aId, sndSecure) <- joinConnection b 1 True connReq "bob's connInfo" SMSubscribe + liftIO $ sndSecure `shouldBe` False + ("", _, REQ invId _ "bob's connInfo") <- get a + (bId, _) <- A.prepareConnectionToAccept a 1 True invId PQSupportOn + sndSecure' <- acceptContact a 1 bId True invId "alice's connInfo" PQSupportOn SMSubscribe + liftIO $ sndSecure' `shouldBe` True + ("", _, CONF confId _ "alice's connInfo") <- get b + allowConnection b aId confId "bob's connInfo" + get a ##> ("", bId, INFO "bob's connInfo") + get a ##> ("", bId, CON) + get b ##> ("", aId, CON) + exchangeGreetings a bId b aId + connReqWithKeys :: BinaryConnectionRequestUri m -> Maybe AddressRatchetKeys -> ConnectionRequestUri m connReqWithKeys cr rk = case cr of BCRInvitationUri crData e2eParams -> CRInvitationUri crData e2eParams