diff --git a/rfcs/2024-03-03-pqdr-version.md b/rfcs/2024-03-03-pqdr-version.md new file mode 100644 index 000000000..5db9f23a5 --- /dev/null +++ b/rfcs/2024-03-03-pqdr-version.md @@ -0,0 +1,92 @@ +# Migrating existing connections to post-quantum double ratchet algorithm + +## Problem + +Post-quantum variant of double ratchet algorithm represents an almost full-stack change affecting all parts of the protocol stack except client-server protocol (SMP): +- double-ratchet end-to-end encryption: different encoding (additional large keys require byte-strings larger than 255 bytes with 2-byte length prefixes) and larger message headers (increased by ~2200 bytes). +- agent-agent protocol: a smaller maximum message size to accomodate larger headers and to fit in 16kb blocks, reduced by ~2200 bytes for the messages and by almost ~4000 bytes for connection information. +- chat protocol: also a smaller message size compensated by zstd comression of JSON messages. + +We want the versioning that achieves these objectives: +- all changes in all protocol layers happen at the same time, when both clients support it. +- ability to downgrade the clients to the previous version without losing connection. +- ability to opt-in into this functionality via "experimental" feature toggle, that enables post-quantum encryption in connections when both contacts enable this toggle. + +To have ability to downgrade the clients we have two options: +- roll-out this functionality in two stages: 1) roll-out clients support but do not enable the new version, and then 2) upgrade client version. The problem here is that the clients won't be able to opt-in into this experiment. +- make offered range dependent on experimental feature being enabled. Currently we have an option to enable PQ encryption in agent API, and this option can be used as a proxy to maxium supported protocol version - if the option is passed, it can be seen as an indication that higher version range (or version) should offered (or accepted). + +## Solution + +Currently ratchet state stores version range. It's unclear what was the intended semantics of that version range - it simply stores the offered/supported version range at the time ratchet was initialised, but only a high bound is used to send in message headers, and it is never upgraded. In JSON this range is encoded as tuple (an array of two elements in JSON). + +We could continue using this range with the meaning of the lower bound to be "currently used ratchet version" and the meaning of higher boundary to be "maximum supported ratchet version". We could also use the version communicated in message headers to upgrade ratchet version, with the condition that upgrade should only happen if both sides want it. Currently it's defined by pqEnableKEM property in ratchet state. We could also make it more explicit by defining maximum version to which ratchet should upgrade. Given that irreversible upgrades are not very common, it is probably ok to keep it implicit. + +We can define a better type than VersionRange to reflect semantics of the range in ratchet (current/max supported range), but for backward compatibility it needs to be encoded in the same way as now. + +To summarize, the proposed solution for ratchet versioning is: +- define ratchet versions as new type to include current and maximum allowed versions, where maximum allowed will be either the same or lower than maximum supported based on PQ option (in 5.6), and in 5.7 it will be changed to maximum supported, so version starts upgrading independently from PQ being enabled. +- make encodings in ratchet depend on current version (in curent code it depends on max version). +- include max allowed in message header. +- upgrade current if in range on each new message if less than max and higher than current (same as we do for connections). +- increase max allowed once PQ is enabled (only in 5.6). Make max allowed the same as max supported (global constant). + +```haskell +data RatchetVR = RatchetVR + { currentVersion :: Version, + maxAllowedVersion :: Version + } + +instance ToJSON RatchetVR where + toEncoding (RatchetVR v1 v2) = toEncoding (v1, v2) + toJSON (RatchetVR v1 v2) = toJSON (v1, v2) + +instance FromJSON RatchetVR where + parseJSON v = do + -- this also verifies that v2 > v1 (although we could remove JSON instances for VersionRange) + VersionRange v1 v2 <- parseJSON v + pure $ RatchetVR v1 v2 +``` + +For connections, we could also make version used for the purposes of encoding dependent on the PQ being enabled, and version for decoding taken from message header, but then we'd have to not only upgrade ratchets but the connection as well every time PQ mode changes. + +Another suggestion to ensure that correct version range is used in correct contexts could be: +- using different newtypes for different version ranges. +- define generic type class for version aware encoding that would also accept only specific type class for the version to use the correct range. This may be justified as there will be several version-aware encodings, and not just the protocol as now. + +```haskell +class Ord v => EncodingV v a where + {-# MINIMAL smpEncodeV, (smpDecodeV | smpVP) #-} + smpEncodeV :: v -> a -> ByteString + -- default decode uses parser + smpDecodeV :: v -> ByteString -> Either String a + smpDecodeV = parseAll . smpVP + -- default parser decodes from length-specified bytestring + smpVP :: v -> Parser a + smpVP v = smpDecodeV v <$?> smpP +``` + +The version will be passed from currently agreed version, it may only change when message is received, not when message is sent. The version will not be extracted from the encoding itself as it happens now in ratchet encodings. + +## Various options how the problem can be simplified + +1. Do not support connection downgrade once both devices upgraded. If applied to all existing connections then it is a bad option, as it would disrupt some important conversations. + +2. Do not provide ability to opt-in into PQ encryption until v5.7 where it will be rolled out automatically. That is also suboptimal, as it won't allow announcing technology design and have testing outside of the team devices. + +3. The logic explained above where connection upgrade and downgrade is possible and applied to all existing connections if both parties consent to it. There are these important downsides: + - complexity of this logic + - regression risks when this logic is removed. + - some non-coordinated upgrades of existing, potentially important conversations, simply because two users opt-in into the experiment without any expectation that another side also opts-in. + +4. Apply upgrade/downgrade logic and enable PQ encryption as opt-in, based on the toggle in the UX, only for the new connections. This seems the least risky, and also simpler than option 3, as it would only apply to the new connections, and both users will have to enable experimental toggle prior to connecting. + +Option 4 seems the best trade-off, and has these sub-options regarding where it is controlled: +a) in chat based on connection flag. Chat will pass PQ options only to connections that were created when experimental option was enabled. +b) in agent - there will be additional logic to ignore PQ option for existing connections. +c) both in chat and in agent. + +Option 4a seems better, as it would: +- simplify agent code +- minimise required changes when releasing v5.7 (as we do want that all direct and small groups connections migrate to PQ encryption at the time, without any toggles) +- allow tests for connection upgrade in the currect code. diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 74d4e1c40..6f3099ad6 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -558,14 +558,14 @@ newConnAsync c userId corrId enableNtfs cMode pqInitKeys subMode = do newConnNoQueues :: AgentMonad m => AgentClient -> UserId -> ConnId -> Bool -> SConnectionMode c -> CR.PQEncryption -> m ConnId newConnNoQueues c userId connId enableNtfs cMode pqEncryption = do g <- asks random - connAgentVersion <- asks $ maxVersion . smpAgentVRange . config + connAgentVersion <- asks $ maxVersion . ($ pqEncryption) . smpAgentVRange . config let cData = ConnData {userId, connId, connAgentVersion, enableNtfs, lastExternalSndId = 0, deleted = False, ratchetSyncState = RSOk, pqEncryption} withStore c $ \db -> createNewConn db g cData cMode joinConnAsync :: AgentMonad m => AgentClient -> UserId -> ACorrId -> Bool -> ConnectionRequestUri c -> ConnInfo -> CR.PQEncryption -> SubscriptionMode -> m ConnId joinConnAsync c userId corrId enableNtfs cReqUri@(CRInvitationUri ConnReqUriData {crAgentVRange} _) cInfo pqEncryption subMode = do withInvLock c (strEncode cReqUri) "joinConnAsync" $ do - aVRange <- asks $ smpAgentVRange . config + aVRange <- asks $ ($ pqEncryption) . smpAgentVRange . config case crAgentVRange `compatibleVersion` aVRange of Just (Compatible connAgentVersion) -> do g <- asks random @@ -667,14 +667,16 @@ newRcvConnSrv c userId connId enableNtfs cMode clientData pqInitKeys subMode srv when enableNtfs $ do ns <- asks ntfSupervisor atomically $ sendNtfSubCommand ns (connId, NSCCreate) - let crData = ConnReqUriData SSSimplex smpAgentVRange [qUri] clientData + let pqEnc = CR.connPQEncryption pqInitKeys + crData = ConnReqUriData SSSimplex (smpAgentVRange pqEnc) [qUri] clientData + e2eVRange = e2eEncryptVRange pqEnc case cMode of SCMContact -> pure (connId, CRContactUri crData) SCMInvitation -> do g <- asks random - (pk1, pk2, pKem, e2eRcvParams) <- liftIO $ CR.generateRcvE2EParams g (maxVersion e2eEncryptVRange) (CR.initialPQEncryption pqInitKeys) + (pk1, pk2, pKem, e2eRcvParams) <- liftIO $ CR.generateRcvE2EParams g (maxVersion e2eVRange) (CR.initialPQEncryption pqInitKeys) withStore' c $ \db -> createRatchetX3dhKeys db connId pk1 pk2 pKem - pure (connId, CRInvitationUri crData $ toVersionRangeT e2eRcvParams e2eEncryptVRange) + pure (connId, CRInvitationUri crData $ toVersionRangeT e2eRcvParams e2eVRange) joinConn :: AgentMonad m => AgentClient -> UserId -> ConnId -> Bool -> ConnectionRequestUri c -> ConnInfo -> CR.PQEncryption -> SubscriptionMode -> m ConnId joinConn c userId connId enableNtfs cReq cInfo pqEnc subMode = do @@ -687,17 +689,18 @@ joinConn c userId connId enableNtfs cReq cInfo pqEnc subMode = do startJoinInvitation :: AgentMonad m => UserId -> ConnId -> Bool -> ConnectionRequestUri 'CMInvitation -> CR.PQEncryption -> m (Compatible VersionSMPA, ConnData, NewSndQueue, CR.Ratchet 'C.X448, CR.SndE2ERatchetParams 'C.X448) startJoinInvitation userId connId enableNtfs (CRInvitationUri ConnReqUriData {crAgentVRange, crSmpQueues = (qUri :| _)} e2eRcvParamsUri) pqEncryption = do AgentConfig {smpClientVRange, smpAgentVRange, e2eEncryptVRange} <- asks config + let e2eVRange = e2eEncryptVRange pqEncryption case ( qUri `compatibleVersion` smpClientVRange, - e2eRcvParamsUri `compatibleVersion` e2eEncryptVRange, - crAgentVRange `compatibleVersion` smpAgentVRange + e2eRcvParamsUri `compatibleVersion` e2eVRange, + crAgentVRange `compatibleVersion` smpAgentVRange pqEncryption ) of (Just qInfo, Just (Compatible e2eRcvParams@(CR.E2ERatchetParams v _ rcDHRr kem_)), Just aVersion@(Compatible connAgentVersion)) -> do g <- asks random (pk1, pk2, pKem, e2eSndParams) <- liftIO $ CR.generateSndE2EParams g v (CR.replyKEM_ pqEncryption kem_) (_, rcDHRs) <- atomically $ C.generateKeyPair g - -- TODO PQ generate KEM keypair if needed - is it done? rcParams <- liftEitherWith cryptoError $ CR.pqX3dhSnd pk1 pk2 pKem e2eRcvParams - let rc = CR.initSndRatchet e2eEncryptVRange rcDHRr rcDHRs rcParams + let rcVs = CR.RVersions {current = v, maxSupported = maxVersion e2eVRange} + rc = CR.initSndRatchet rcVs rcDHRr rcDHRs rcParams q <- newSndQueue userId "" qInfo let cData = ConnData {userId, connId, connAgentVersion, enableNtfs, lastExternalSndId = 0, deleted = False, ratchetSyncState = RSOk, pqEncryption} pure (aVersion, cData, q, rc, e2eSndParams) @@ -720,7 +723,7 @@ joinConnSrv c userId connId enableNtfs inv@CRInvitationUri {} cInfo pqEnc subMod void $ withStore' c $ \db -> deleteConn db Nothing connId' throwError e joinConnSrv c userId connId enableNtfs (CRContactUri ConnReqUriData {crAgentVRange, crSmpQueues = (qUri :| _)}) cInfo pqEnc subMode srv = do - aVRange <- asks $ smpAgentVRange . config + aVRange <- asks $ ($ pqEnc) . smpAgentVRange . config clientVRange <- asks $ smpClientVRange . config case ( qUri `compatibleVersion` clientVRange, crAgentVRange `compatibleVersion` aVRange @@ -1107,23 +1110,24 @@ enqueueMessage c cData sq pqEnc_ msgFlags aMessage = -- this function is used only for sending messages in batch, it returns the list of successes to enqueue additional deliveries enqueueMessageB :: forall m t. (AgentMonad' m, Traversable t) => AgentClient -> t (Either AgentErrorType (ConnData, NonEmpty SndQueue, Maybe CR.PQEncryption, MsgFlags, AMessage)) -> m (t (Either AgentErrorType ((AgentMsgId, CR.PQEncryption), Maybe (ConnData, [SndQueue], AgentMsgId)))) enqueueMessageB c reqs = do - aVRange <- asks $ maxVersion . smpAgentVRange . config - reqMids <- withStoreBatch c $ \db -> fmap (bindRight $ storeSentMsg db aVRange) reqs + getAVRange <- asks $ smpAgentVRange . config + reqMids <- withStoreBatch c $ \db -> fmap (bindRight $ storeSentMsg db getAVRange) reqs forME reqMids $ \((cData, sq :| sqs, _, _, _), InternalId msgId, pqSecr) -> do submitPendingMsg c cData sq let sqs' = filter isActiveSndQ sqs pure $ Right ((msgId, pqSecr), if null sqs' then Nothing else Just (cData, sqs', msgId)) where - storeSentMsg :: DB.Connection -> VersionSMPA -> (ConnData, NonEmpty SndQueue, Maybe CR.PQEncryption, MsgFlags, AMessage) -> IO (Either AgentErrorType ((ConnData, NonEmpty SndQueue, Maybe CR.PQEncryption, MsgFlags, AMessage), InternalId, CR.PQEncryption)) - storeSentMsg db agentVersion req@(ConnData {connId}, sq :| _, pqEnc_, msgFlags, aMessage) = fmap (first storeError) $ runExceptT $ do + storeSentMsg :: DB.Connection -> (CR.PQEncryption -> VersionRangeSMPA) -> (ConnData, NonEmpty SndQueue, Maybe CR.PQEncryption, MsgFlags, AMessage) -> IO (Either AgentErrorType ((ConnData, NonEmpty SndQueue, Maybe CR.PQEncryption, MsgFlags, AMessage), InternalId, CR.PQEncryption)) + storeSentMsg db getAVRange req@(ConnData {connId, connAgentVersion = v}, sq :| _, pqEnc_, msgFlags, aMessage) = fmap (first storeError) $ runExceptT $ do internalTs <- liftIO getCurrentTime (internalId, internalSndId, prevMsgHash) <- liftIO $ updateSndIds db connId let privHeader = APrivHeader (unSndId internalSndId) prevMsgHash agentMsg = AgentMessage privHeader aMessage agentMsgStr = smpEncode agentMsg internalHash = C.sha256Hash agentMsgStr - (encAgentMessage, pqEncryption) <- agentRatchetEncrypt db connId agentMsgStr e2eEncUserMsgLength pqEnc_ - let msgBody = smpEncode $ AgentMsgEnvelope {agentVersion, encAgentMessage} + (encAgentMessage, pqEncryption) <- agentRatchetEncrypt db connId agentMsgStr (e2eEncUserMsgLength v) pqEnc_ + let agentVersion = maxVersion . getAVRange $ fromMaybe CR.PQEncOff pqEnc_ + msgBody = smpEncode $ AgentMsgEnvelope {agentVersion, encAgentMessage} msgType = agentMessageType agentMsg msgData = SndMsgData {internalId, internalSndId, internalTs, msgType, msgFlags, msgBody, pqEncryption, internalHash, prevMsgHash} liftIO $ createSndMsg db connId msgData @@ -1402,18 +1406,19 @@ abortConnectionSwitch' c connId = synchronizeRatchet' :: AgentMonad m => AgentClient -> ConnId -> CR.PQEncryption -> Bool -> m ConnectionStats synchronizeRatchet' c connId pqEnc force = withConnLock c connId "synchronizeRatchet" $ do withStore c (`getConn` connId) >>= \case - SomeConn _ (DuplexConnection cData rqs sqs) + SomeConn _ (DuplexConnection cData@ConnData {pqEncryption} rqs sqs) | ratchetSyncAllowed cData || force -> do -- check queues are not switching? + cData' <- if pqEncryption == pqEnc then pure cData else withStore' c $ \db -> setConnPQEncryption db cData pqEnc AgentConfig {e2eEncryptVRange} <- asks config g <- asks random - (pk1, pk2, pKem, e2eParams) <- liftIO $ CR.generateRcvE2EParams g (maxVersion e2eEncryptVRange) pqEnc - enqueueRatchetKeyMsgs c cData sqs e2eParams + (pk1, pk2, pKem, e2eParams) <- liftIO $ CR.generateRcvE2EParams g (maxVersion $ e2eEncryptVRange pqEnc) pqEnc + enqueueRatchetKeyMsgs c cData' sqs e2eParams withStore' c $ \db -> do setConnRatchetSync db connId RSStarted setRatchetX3dhKeys db connId pk1 pk2 pKem - let cData' = cData {ratchetSyncState = RSStarted} :: ConnData - conn' = DuplexConnection cData' rqs sqs + let cData'' = cData' {ratchetSyncState = RSStarted} :: ConnData + conn' = DuplexConnection cData'' rqs sqs pure $ connectionStats conn' | otherwise -> throwError $ CMD PROHIBITED _ -> throwError $ CMD PROHIBITED @@ -2064,8 +2069,8 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v, _ -> prohibited >> ack _ -> prohibited >> ack updateConnVersion :: Connection c -> ConnData -> VersionSMPA -> m (Connection c) - updateConnVersion conn' cData' msgAgentVersion = do - aVRange <- asks $ smpAgentVRange . config + updateConnVersion conn' cData'@ConnData {pqEncryption} msgAgentVersion = do + aVRange <- asks $ ($ pqEncryption) . smpAgentVRange . config let msgAVRange = fromMaybe (versionToRange msgAgentVersion) $ safeVersionRange (minVersion aVRange) msgAgentVersion case msgAVRange `compatibleVersion` aVRange of Just (Compatible av) @@ -2126,21 +2131,27 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v, parseMessage :: Encoding a => ByteString -> m a parseMessage = liftEither . parse smpP (AGENT A_MESSAGE) + -- TODO PQ make sure pqEncryption in conn' is set correctly smpConfirmation :: SMP.MsgId -> Connection c -> C.APublicAuthKey -> C.PublicKeyX25519 -> Maybe (CR.SndE2ERatchetParams 'C.X448) -> ByteString -> VersionSMPC -> VersionSMPA -> m () smpConfirmation srvMsgId conn' senderKey e2ePubKey e2eEncryption encConnInfo smpClientVersion agentVersion = do logServer "<--" c srv rId $ "MSG :" <> logSecret srvMsgId AgentConfig {smpClientVRange, smpAgentVRange, e2eEncryptVRange} <- asks config + let ConnData {pqEncryption} = toConnData conn' + aVRange = smpAgentVRange pqEncryption + e2eVRange = e2eEncryptVRange pqEncryption unless - (agentVersion `isCompatible` smpAgentVRange && smpClientVersion `isCompatible` smpClientVRange) + (agentVersion `isCompatible` aVRange && smpClientVersion `isCompatible` smpClientVRange) (throwError $ AGENT A_VERSION) case status of New -> case (conn', e2eEncryption) of -- party initiating connection - (RcvConnection ConnData {pqEncryption} _, Just (CR.AE2ERatchetParams _ e2eSndParams@(CR.E2ERatchetParams e2eVersion _ _ _))) -> do - unless (e2eVersion `isCompatible` e2eEncryptVRange) (throwError $ AGENT A_VERSION) + (RcvConnection _ _, Just (CR.AE2ERatchetParams _ e2eSndParams@(CR.E2ERatchetParams e2eVersion _ _ _))) -> do + unless (e2eVersion `isCompatible` e2eVRange) (throwError $ AGENT A_VERSION) (pk1, rcDHRs, pKem) <- withStore c (`getRatchetX3dhKeys` connId) rcParams <- liftError cryptoError $ CR.pqX3dhRcv pk1 rcDHRs pKem e2eSndParams - let rc = CR.initRcvRatchet e2eEncryptVRange rcDHRs rcParams pqEncryption + -- TODO PQ combine isCompatible check and construction in one call + let rcVs = CR.RVersions {current = e2eVersion, maxSupported = maxVersion e2eVRange} + rc = CR.initRcvRatchet rcVs rcDHRs rcParams pqEncryption g <- asks random (agentMsgBody_, rc', skipped) <- liftError cryptoError $ CR.rcDecrypt g rc M.empty encConnInfo case (agentMsgBody_, skipped) of @@ -2153,7 +2164,7 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v, processConf connInfo senderConf = do let newConfirmation = NewConfirmation {connId, senderConf, ratchetState = rc'} confId <- withStore c $ \db -> do - setConnectionVersion db connId agentVersion + setConnAgentVersion db connId agentVersion createConfirmation db g newConfirmation let srvs = map qServer $ smpReplyQueues senderConf notify $ CONF confId srvs connInfo @@ -2182,8 +2193,6 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v, -- `sndStatus == Active` when HELLO was previously sent, and this is the reply HELLO -- this branch is executed by the accepting party in duplexHandshake mode (v2) -- (was executed by initiating party in v1 that is no longer supported) - -- - -- TODO PQ encryption mode | sndStatus == Active -> notify $ CON pqEncryption | otherwise -> enqueueDuplexHello sq _ -> pure () @@ -2328,13 +2337,17 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v, DuplexConnection {} -> action conn' _ -> qError $ name <> ": message must be sent to duplex connection" + -- TODO PQ make sure pqEncryption is set correctly here newRatchetKey :: CR.RcvE2ERatchetParams 'C.X448 -> Connection 'CDuplex -> m () - newRatchetKey e2eOtherPartyParams@(CR.E2ERatchetParams e2eVersion k1Rcv k2Rcv kem_) conn'@(DuplexConnection cData'@ConnData {lastExternalSndId} _ sqs) = + newRatchetKey e2eOtherPartyParams@(CR.E2ERatchetParams e2eVersion k1Rcv k2Rcv _) conn'@(DuplexConnection cData'@ConnData {lastExternalSndId, pqEncryption} _ sqs) = unlessM ratchetExists $ do AgentConfig {e2eEncryptVRange} <- asks config - unless (e2eVersion `isCompatible` e2eEncryptVRange) (throwError $ AGENT A_VERSION) + let connE2EVRange = e2eEncryptVRange pqEncryption + unless (e2eVersion `isCompatible` connE2EVRange) (throwError $ AGENT A_VERSION) keys <- getSendRatchetKeys - initRatchet e2eEncryptVRange keys + -- TODO PQ combine with `isCompatible` check above + let rcVs = CR.RVersions {current = e2eVersion, maxSupported = maxVersion connE2EVRange} + initRatchet rcVs keys notifyAgreed where rkHashRcv = rkHash k1Rcv k2Rcv @@ -2360,8 +2373,7 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v, where sendReplyKey = do g <- asks random - -- TODO PQ the decision to use KEM should depend on connection - (pk1, pk2, pKem, e2eParams) <- liftIO $ CR.generateRcvE2EParams g e2eVersion CR.PQEncOn + (pk1, pk2, pKem, e2eParams) <- liftIO $ CR.generateRcvE2EParams g e2eVersion pqEncryption enqueueRatchetKeyMsgs c cData' sqs e2eParams pure (pk1, pk2, pKem) notifyRatchetSyncError = do @@ -2380,16 +2392,15 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v, createRatchet db connId rc -- compare public keys `k1` in AgentRatchetKey messages sent by self and other party -- to determine ratchet initilization ordering - initRatchet :: CR.VersionRangeE2E -> (C.PrivateKeyX448, C.PrivateKeyX448, Maybe CR.RcvPrivRKEMParams) -> m () - initRatchet e2eEncryptVRange (pk1, pk2, pKem) + initRatchet :: CR.RatchetVersions -> (C.PrivateKeyX448, C.PrivateKeyX448, Maybe CR.RcvPrivRKEMParams) -> m () + initRatchet rcVs (pk1, pk2, pKem) | rkHash (C.publicKey pk1) (C.publicKey pk2) <= rkHashRcv = do rcParams <- liftError cryptoError $ CR.pqX3dhRcv pk1 pk2 pKem e2eOtherPartyParams - -- TODO PQ the decision to use KEM should either depend on the global setting or on whether it was enabled in connection before - recreateRatchet $ CR.initRcvRatchet e2eEncryptVRange pk2 rcParams $ CR.PQEncryption (isJust kem_) + recreateRatchet $ CR.initRcvRatchet rcVs pk2 rcParams pqEncryption | otherwise = do (_, rcDHRs) <- atomically . C.generateKeyPair =<< asks random rcParams <- liftEitherWith cryptoError $ CR.pqX3dhSnd pk1 pk2 (CR.APRKP CR.SRKSProposed <$> pKem) e2eOtherPartyParams - recreateRatchet $ CR.initSndRatchet e2eEncryptVRange k2Rcv rcDHRs rcParams + recreateRatchet $ CR.initSndRatchet rcVs k2Rcv rcDHRs rcParams void . enqueueMessages' c cData' sqs Nothing SMP.MsgFlags {notification = True} $ EREADY lastExternalSndId checkMsgIntegrity :: PrevExternalSndId -> ExternalSndId -> PrevRcvMsgHash -> ByteString -> MsgIntegrity @@ -2432,7 +2443,7 @@ confirmQueueAsync c cData sq srv connInfo e2eEncryption_ pqEnc subMode = do submitPendingMsg c cData sq confirmQueue :: forall m. AgentMonad m => Compatible VersionSMPA -> AgentClient -> ConnData -> SndQueue -> SMPServerWithAuth -> ConnInfo -> Maybe (CR.SndE2ERatchetParams 'C.X448) -> Maybe CR.PQEncryption -> SubscriptionMode -> m () -confirmQueue (Compatible agentVersion) c cData@ConnData {connId} sq srv connInfo e2eEncryption_ pqEnc_ subMode = do +confirmQueue (Compatible agentVersion) c cData@ConnData {connId, connAgentVersion = v} sq srv connInfo e2eEncryption_ pqEnc_ subMode = do msg <- mkConfirmation =<< mkAgentConfirmation c cData sq srv connInfo subMode sendConfirmation c sq msg withStore' c $ \db -> setSndQueueStatus db sq Confirmed @@ -2440,7 +2451,7 @@ confirmQueue (Compatible agentVersion) c cData@ConnData {connId} sq srv connInfo mkConfirmation :: AgentMessage -> m MsgBody mkConfirmation aMessage = withStore c $ \db -> runExceptT $ do void . liftIO $ updateSndIds db connId - (encConnInfo, _) <- agentRatchetEncrypt db connId (smpEncode aMessage) e2eEncConnInfoLength pqEnc_ + (encConnInfo, _) <- agentRatchetEncrypt db connId (smpEncode aMessage) (e2eEncConnInfoLength v) pqEnc_ pure . smpEncode $ AgentConfirmation {agentVersion, e2eEncryption_, encConnInfo} mkAgentConfirmation :: AgentMonad m => AgentClient -> ConnData -> SndQueue -> SMPServerWithAuth -> ConnInfo -> SubscriptionMode -> m AgentMessage @@ -2454,13 +2465,13 @@ enqueueConfirmation c cData sq connInfo e2eEncryption_ pqEnc_ = do submitPendingMsg c cData sq storeConfirmation :: AgentMonad m => AgentClient -> ConnData -> SndQueue -> Maybe (CR.SndE2ERatchetParams 'C.X448) -> Maybe CR.PQEncryption -> AgentMessage -> m () -storeConfirmation c ConnData {connId, connAgentVersion} sq e2eEncryption_ pqEnc_ agentMsg = withStore c $ \db -> runExceptT $ do +storeConfirmation c ConnData {connId, connAgentVersion = v} sq e2eEncryption_ pqEnc_ agentMsg = withStore c $ \db -> runExceptT $ do internalTs <- liftIO getCurrentTime (internalId, internalSndId, prevMsgHash) <- liftIO $ updateSndIds db connId let agentMsgStr = smpEncode agentMsg internalHash = C.sha256Hash agentMsgStr - (encConnInfo, pqEncryption) <- agentRatchetEncrypt db connId agentMsgStr e2eEncConnInfoLength pqEnc_ - let msgBody = smpEncode $ AgentConfirmation {agentVersion = connAgentVersion, e2eEncryption_, encConnInfo} + (encConnInfo, pqEncryption) <- agentRatchetEncrypt db connId agentMsgStr (e2eEncConnInfoLength v) pqEnc_ + let msgBody = smpEncode $ AgentConfirmation {agentVersion = v, e2eEncryption_, encConnInfo} msgType = agentMessageType agentMsg msgData = SndMsgData {internalId, internalSndId, internalTs, msgType, msgBody, pqEncryption, msgFlags = SMP.MsgFlags {notification = True}, internalHash, prevMsgHash} liftIO $ createSndMsg db connId msgData @@ -2472,8 +2483,8 @@ enqueueRatchetKeyMsgs c cData (sq :| sqs) e2eEncryption = do mapM_ (enqueueSavedMessage c cData msgId) $ filter isActiveSndQ sqs enqueueRatchetKey :: forall m. AgentMonad m => AgentClient -> ConnData -> SndQueue -> CR.RcvE2ERatchetParams 'C.X448 -> m AgentMsgId -enqueueRatchetKey c cData@ConnData {connId} sq e2eEncryption = do - aVRange <- asks $ smpAgentVRange . config +enqueueRatchetKey c cData@ConnData {connId, pqEncryption} sq e2eEncryption = do + aVRange <- asks $ ($ pqEncryption) . smpAgentVRange . config msgId <- storeRatchetKey $ maxVersion aVRange submitPendingMsg c cData sq pure $ unId msgId @@ -2487,8 +2498,7 @@ enqueueRatchetKey c cData@ConnData {connId} sq e2eEncryption = do internalHash = C.sha256Hash agentMsgStr let msgBody = smpEncode $ AgentRatchetKey {agentVersion, e2eEncryption, info = agentMsgStr} msgType = agentMessageType agentMsg - -- TODO PQ set pqEncryption based on connection mode - msgData = SndMsgData {internalId, internalSndId, internalTs, msgType, msgBody, pqEncryption = CR.PQEncOff, msgFlags = SMP.MsgFlags {notification = True}, internalHash, prevMsgHash} + msgData = SndMsgData {internalId, internalSndId, internalTs, msgType, msgBody, pqEncryption, msgFlags = SMP.MsgFlags {notification = True}, internalHash, prevMsgHash} liftIO $ createSndMsg db connId msgData liftIO $ createSndMsgDelivery db connId sq internalId pure internalId diff --git a/src/Simplex/Messaging/Agent/Env/SQLite.hs b/src/Simplex/Messaging/Agent/Env/SQLite.hs index 7a879bb22..a292e5db6 100644 --- a/src/Simplex/Messaging/Agent/Env/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Env/SQLite.hs @@ -56,15 +56,14 @@ import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations import Simplex.Messaging.Client import Simplex.Messaging.Client.Agent () import qualified Simplex.Messaging.Crypto as C -import Simplex.Messaging.Crypto.Ratchet (VersionRangeE2E, supportedE2EEncryptVRange) +import Simplex.Messaging.Crypto.Ratchet (PQEncryption, VersionRangeE2E, supportedE2EEncryptVRange) import Simplex.Messaging.Notifications.Client (defaultNTFClientConfig) import Simplex.Messaging.Notifications.Transport (NTFVersion) import Simplex.Messaging.Notifications.Types import Simplex.Messaging.Protocol (NtfServer, VersionRangeSMPC, XFTPServer, XFTPServerWithAuth, supportedSMPClientVRange) -import Simplex.Messaging.Transport (SMPVersion) import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM -import Simplex.Messaging.Transport (TLS, Transport (..)) +import Simplex.Messaging.Transport (SMPVersion, TLS, Transport (..)) import Simplex.Messaging.Transport.Client (defaultSMPPort) import Simplex.Messaging.Util (allFinally, catchAllErrors, tryAllErrors) import System.Random (StdGen, newStdGen) @@ -117,8 +116,8 @@ data AgentConfig = AgentConfig caCertificateFile :: FilePath, privateKeyFile :: FilePath, certificateFile :: FilePath, - e2eEncryptVRange :: VersionRangeE2E, - smpAgentVRange :: VersionRangeSMPA, + e2eEncryptVRange :: PQEncryption -> VersionRangeE2E, + smpAgentVRange :: PQEncryption -> VersionRangeSMPA, smpClientVRange :: VersionRangeSMPC } diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index b4383cf9b..af271531b 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -39,6 +39,7 @@ module Simplex.Messaging.Agent.Protocol pattern VersionSMPA, ratchetSyncSMPAgentVersion, deliveryRcptsSMPAgentVersion, + pqdrSMPAgentVersion, supportedSMPAgentVRange, e2eEncConnInfoLength, e2eEncUserMsgLength, @@ -187,7 +188,15 @@ import Simplex.FileTransfer.Protocol (FileParty (..)) import Simplex.FileTransfer.Transport (XFTPErrorType) import Simplex.Messaging.Agent.QueryString import qualified Simplex.Messaging.Crypto as C -import Simplex.Messaging.Crypto.Ratchet (InitialKeys (..), PQEncryption (..), pattern PQEncOff, RcvE2ERatchetParams, RcvE2ERatchetParamsUri, SndE2ERatchetParams) +import Simplex.Messaging.Crypto.Ratchet + ( InitialKeys (..), + PQEncryption (..), + pattern PQEncOff, + pattern PQEncOn, + RcvE2ERatchetParams, + RcvE2ERatchetParamsUri, + SndE2ERatchetParams + ) import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers @@ -234,6 +243,7 @@ import UnliftIO.Exception (Exception) -- 2 - "duplex" (more efficient) connection handshake (6/9/2022) -- 3 - support ratchet renegotiation (6/30/2023) -- 4 - delivery receipts (7/13/2023) +-- 5 - post-quantum double ratchet (3/14/2024) data SMPAgentVersion @@ -255,24 +265,34 @@ ratchetSyncSMPAgentVersion = VersionSMPA 3 deliveryRcptsSMPAgentVersion :: VersionSMPA deliveryRcptsSMPAgentVersion = VersionSMPA 4 +pqdrSMPAgentVersion :: VersionSMPA +pqdrSMPAgentVersion = VersionSMPA 5 + +-- TODO v5.7 increase to 5 currentSMPAgentVersion :: VersionSMPA currentSMPAgentVersion = VersionSMPA 4 -supportedSMPAgentVRange :: VersionRangeSMPA -supportedSMPAgentVRange = mkVersionRange duplexHandshakeSMPAgentVersion currentSMPAgentVersion +-- TODO v5.7 remove dependency of version range on whether PQ encryption is used +supportedSMPAgentVRange :: PQEncryption -> VersionRangeSMPA +supportedSMPAgentVRange pq = + mkVersionRange duplexHandshakeSMPAgentVersion $ case pq of + PQEncOn -> pqdrSMPAgentVersion + PQEncOff -> currentSMPAgentVersion -- it is shorter to allow all handshake headers, -- including E2E (double-ratchet) parameters and -- signing key of the sender for the server --- TODO PQ this should be version-dependent --- previously it was 14848, reduced by 3700 (roughly the increase of message ratchet header size + key and ciphertext in reply link) -e2eEncConnInfoLength :: Int -e2eEncConnInfoLength = 11148 +e2eEncConnInfoLength :: VersionSMPA -> Int +e2eEncConnInfoLength v + -- reduced by 3700 (roughly the increase of message ratchet header size + key and ciphertext in reply link) + | v >= pqdrSMPAgentVersion = 11148 + | otherwise = 14848 --- TODO PQ this should be version-dependent --- previously it was 15856, reduced by 2200 (roughly the increase of message ratchet header size) -e2eEncUserMsgLength :: Int -e2eEncUserMsgLength = 13656 +e2eEncUserMsgLength :: VersionSMPA -> Int +e2eEncUserMsgLength v + -- reduced by 2200 (roughly the increase of message ratchet header size) + | v >= pqdrSMPAgentVersion = 13656 + | otherwise = 15856 -- | Raw (unparsed) SMP agent protocol transmission. type ARawTransmission = (ByteString, ByteString, ByteString) diff --git a/src/Simplex/Messaging/Agent/Store/SQLite.hs b/src/Simplex/Messaging/Agent/Store/SQLite.hs index a2d01c201..78660ce6c 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite.hs @@ -58,6 +58,7 @@ module Simplex.Messaging.Agent.Store.SQLite getConnData, setConnDeleted, setConnAgentVersion, + setConnPQEncryption, getDeletedConnIds, getDeletedWaitingDeliveryConnIds, setConnRatchetSync, @@ -93,7 +94,6 @@ module Simplex.Messaging.Agent.Store.SQLite getAcceptedConfirmation, removeConfirmations, -- Invitations - sent via Contact connections - setConnectionVersion, createInvitation, getInvitation, acceptInvitation, @@ -889,10 +889,6 @@ removeConfirmations db connId = |] [":conn_id" := connId] -setConnectionVersion :: DB.Connection -> ConnId -> VersionSMPA -> IO () -setConnectionVersion db connId aVersion = - DB.execute db "UPDATE connections SET smp_agent_version = ? WHERE conn_id = ?" (aVersion, connId) - createInvitation :: DB.Connection -> TVar ChaChaDRG -> NewInvitation -> IO (Either StoreError InvitationId) createInvitation db gVar NewInvitation {contactConnId, connReq, recipientConnInfo} = createWithRandomId gVar $ \invitationId -> @@ -1956,6 +1952,11 @@ setConnAgentVersion :: DB.Connection -> ConnId -> VersionSMPA -> IO () setConnAgentVersion db connId aVersion = DB.execute db "UPDATE connections SET smp_agent_version = ? WHERE conn_id = ?" (aVersion, connId) +setConnPQEncryption :: DB.Connection -> ConnData -> CR.PQEncryption -> IO ConnData +setConnPQEncryption db cData@ConnData {connId} pqEnc = do + DB.execute db "UPDATE connections SET pq_encryption = ? WHERE conn_id = ?" (pqEnc, connId) + pure (cData :: ConnData) {pqEncryption = pqEnc} + getDeletedConnIds :: DB.Connection -> IO [ConnId] getDeletedConnIds db = map fromOnly <$> DB.query db "SELECT conn_id FROM connections WHERE deleted = ?" (Only True) diff --git a/src/Simplex/Messaging/Crypto/Ratchet.hs b/src/Simplex/Messaging/Crypto/Ratchet.hs index a6faf49c7..9d4b919ca 100644 --- a/src/Simplex/Messaging/Crypto/Ratchet.hs +++ b/src/Simplex/Messaging/Crypto/Ratchet.hs @@ -23,6 +23,8 @@ module Simplex.Messaging.Crypto.Ratchet SkippedMsgDiff (..), SkippedMsgKeys, InitialKeys (..), + pattern IKPQOn, + pattern IKPQOff, PQEncryption (..), pattern PQEncOn, pattern PQEncOff, @@ -40,8 +42,9 @@ module Simplex.Messaging.Crypto.Ratchet VersionE2E, VersionRangeE2E, pattern VersionE2E, + RatchetVersions (..), kdfX3DHE2EEncryptVersion, - pqRatchetVersion, + pqRatchetE2EEncryptVersion, currentE2EEncryptVersion, supportedE2EEncryptVRange, generateRcvE2EParams, @@ -58,7 +61,6 @@ module Simplex.Messaging.Crypto.Ratchet rcDecrypt, -- used in tests MsgHeader (..), - RatchetVersions (..), RatchetInitParams (..), UseKEM (..), RKEMParams (..), @@ -71,6 +73,8 @@ module Simplex.Messaging.Crypto.Ratchet ratchetVersions, fullHeaderLen, applySMDiff, + encodeMsgHeader, + msgHeaderP, ) where @@ -85,7 +89,7 @@ import Crypto.Random (ChaChaDRG) import Data.Aeson (FromJSON (..), ToJSON (..)) import qualified Data.Aeson as J import qualified Data.Aeson.TH as JQ -import Data.Attoparsec.ByteString (Parser) +import Data.Attoparsec.ByteString (Parser, peekWord8') import qualified Data.Attoparsec.ByteString.Char8 as A import qualified Data.ByteArray as BA import Data.ByteString.Char8 (ByteString) @@ -131,14 +135,19 @@ pattern VersionE2E v = Version v kdfX3DHE2EEncryptVersion :: VersionE2E kdfX3DHE2EEncryptVersion = VersionE2E 2 -pqRatchetVersion :: VersionE2E -pqRatchetVersion = VersionE2E 3 +pqRatchetE2EEncryptVersion :: VersionE2E +pqRatchetE2EEncryptVersion = VersionE2E 3 +-- TODO v5.7 increase to 3 currentE2EEncryptVersion :: VersionE2E -currentE2EEncryptVersion = VersionE2E 3 +currentE2EEncryptVersion = VersionE2E 2 -supportedE2EEncryptVRange :: VersionRangeE2E -supportedE2EEncryptVRange = mkVersionRange kdfX3DHE2EEncryptVersion currentE2EEncryptVersion +-- TODO v5.7 remove dependency of version range on whether PQ encryption is used +supportedE2EEncryptVRange :: PQEncryption -> VersionRangeE2E +supportedE2EEncryptVRange pq = + mkVersionRange kdfX3DHE2EEncryptVersion $ case pq of + PQEncOn -> pqRatchetE2EEncryptVersion + PQEncOff -> currentE2EEncryptVersion data RatchetKEMState = RKSProposed -- only KEM encapsulation key @@ -215,7 +224,7 @@ deriving instance Show AnyE2ERatchetParams instance (RatchetKEMStateI s, AlgorithmI a) => Encoding (E2ERatchetParams s a) where smpEncode (E2ERatchetParams v k1 k2 kem_) - | v >= pqRatchetVersion = smpEncode (v, k1, k2, kem_) + | v >= pqRatchetE2EEncryptVersion = smpEncode (v, k1, k2, kem_) | otherwise = smpEncode (v, k1, k2) smpP = toParams <$?> smpP where @@ -243,7 +252,7 @@ instance Encoding AnyE2ERatchetParams where where kemP :: VersionE2E -> Parser (Maybe ARKEMParams) kemP v - | v >= pqRatchetVersion = smpP + | v >= pqRatchetE2EEncryptVersion = smpP | otherwise = pure Nothing instance VersionI E2EVersion (E2ERatchetParams s a) where @@ -283,7 +292,7 @@ instance (RatchetKEMStateI s, AlgorithmI a) => StrEncoding (E2ERatchetParamsUri <> maybe [] encodeKem kem_ where encodeKem kem - | maxVersion vs < pqRatchetVersion = [] + | maxVersion vs < pqRatchetE2EEncryptVersion = [] | otherwise = case kem of RKParamsProposed k -> [("kem_key", strEncode k)] RKParamsAccepted ct k -> [("kem_ct", strEncode ct), ("kem_key", strEncode k)] @@ -313,7 +322,7 @@ instance StrEncoding AnyE2ERatchetParamsUri where _ -> fail "bad e2e params" where kemP vr query - | maxVersion vr >= pqRatchetVersion = + | maxVersion vr >= pqRatchetE2EEncryptVersion = queryParam_ "kem_key" query $>>= \k -> Just . kemParams k <$> queryParam_ "kem_ct" query | otherwise = pure Nothing @@ -366,7 +375,7 @@ generateE2EParams g v useKEM_ = do where kemParams :: IO (Maybe (RKEMParams s, PrivRKEMParams s)) kemParams = case useKEM_ of - Just useKem | v >= pqRatchetVersion -> Just <$> do + Just useKem | v >= pqRatchetE2EEncryptVersion -> Just <$> do ks@(k, _) <- sntrup761Keypair g case useKem of ProposeKEM -> pure (RKParamsProposed k, PrivateRKParamsProposed ks) @@ -414,7 +423,7 @@ pqX3dhSnd spk1 spk2 spKem_ (E2ERatchetParams v rk1 rk2 rKem_) = do where sndPq :: Either CryptoError (Maybe KEMKeyPair, Maybe RatchetKEMAccepted) sndPq = case spKem_ of - Just (APRKP _ ps) | v >= pqRatchetVersion -> case (ps, rKem_) of + Just (APRKP _ ps) | v >= pqRatchetE2EEncryptVersion -> case (ps, rKem_) of (PrivateRKParamsAccepted ct shared ks, Just (RKParamsProposed k)) -> Right (Just ks, Just $ RatchetKEMAccepted k shared ct) (PrivateRKParamsProposed ks, _) -> Right (Just ks, Nothing) -- both parties can send "proposal" in case of ratchet renegotiation _ -> Left CERatchetKEMState @@ -430,7 +439,7 @@ pqX3dhRcv rpk1 rpk2 rpKem_ (E2ERatchetParams v sk1 sk2 sKem_) = do where rcvPq :: ExceptT CryptoError IO (Maybe (KEMKeyPair, RatchetKEMAccepted)) rcvPq = case sKem_ of - Just (RKParamsAccepted ct k') | v >= pqRatchetVersion -> case rpKem_ of + Just (RKParamsAccepted ct k') | v >= pqRatchetE2EEncryptVersion -> case rpKem_ of Just (PrivateRKParamsProposed ks@(_, pk)) -> do shared <- liftIO $ sntrup761Dec ct pk pure $ Just (ks, RatchetKEMAccepted k' shared ct) @@ -457,7 +466,6 @@ data Ratchet a = Ratchet rcAD :: Str, rcDHRs :: PrivateKey a, rcKEM :: Maybe RatchetKEM, - -- TODO PQ make them optional via JSON parser for PQEncryption rcEnableKEM :: PQEncryption, -- will enable KEM on the next ratchet step rcSndKEM :: PQEncryption, -- used KEM hybrid secret for sending ratchet rcRcvKEM :: PQEncryption, -- used KEM hybrid secret for receiving ratchet @@ -584,12 +592,12 @@ instance FromField MessageKey where fromField = blobFieldDecoder smpDecode -- // above added for KEM -- @ initSndRatchet :: - forall a. (AlgorithmI a, DhAlgorithm a) => VersionRangeE2E -> PublicKey a -> PrivateKey a -> (RatchetInitParams, Maybe KEMKeyPair) -> Ratchet a -initSndRatchet v rcDHRr rcDHRs (RatchetInitParams {assocData, ratchetKey, sndHK, rcvNextHK, kemAccepted}, rcPQRs_) = do + forall a. (AlgorithmI a, DhAlgorithm a) => RatchetVersions -> PublicKey a -> PrivateKey a -> (RatchetInitParams, Maybe KEMKeyPair) -> Ratchet a +initSndRatchet rcVersion rcDHRr rcDHRs (RatchetInitParams {assocData, ratchetKey, sndHK, rcvNextHK, kemAccepted}, rcPQRs_) = do -- state.RK, state.CKs, state.NHKs = KDF_RK_HE(SK, DH(state.DHRs, state.DHRr) || state.PQRss) let (rcRK, rcCKs, rcNHKs) = rootKdf ratchetKey rcDHRr rcDHRs (rcPQRss <$> kemAccepted) in Ratchet - { rcVersion = ratchetVersions v, + { rcVersion, rcAD = assocData, rcDHRs, rcKEM = (`RatchetKEM` kemAccepted) <$> rcPQRs_, @@ -613,10 +621,10 @@ initSndRatchet v rcDHRr rcDHRs (RatchetInitParams {assocData, ratchetKey, sndHK, -- Please note that the public part of rcDHRs was sent to the sender -- as part of the connection request and random salt was received from the sender. initRcvRatchet :: - forall a. (AlgorithmI a, DhAlgorithm a) => VersionRangeE2E -> PrivateKey a -> (RatchetInitParams, Maybe KEMKeyPair) -> PQEncryption -> Ratchet a -initRcvRatchet v rcDHRs (RatchetInitParams {assocData, ratchetKey, sndHK, rcvNextHK, kemAccepted}, rcPQRs_) rcEnableKEM = + forall a. (AlgorithmI a, DhAlgorithm a) => RatchetVersions -> PrivateKey a -> (RatchetInitParams, Maybe KEMKeyPair) -> PQEncryption -> Ratchet a +initRcvRatchet rcVersion rcDHRs (RatchetInitParams {assocData, ratchetKey, sndHK, rcvNextHK, kemAccepted}, rcPQRs_) rcEnableKEM = Ratchet - { rcVersion = ratchetVersions v, + { rcVersion, rcAD = assocData, rcDHRs, -- rcKEM: @@ -654,56 +662,69 @@ data MsgHeader a = MsgHeader -- 69 = 2 (original size) + 2 + 1+56 (Curve448) + 4 + 4 -- TODO PQ this must be version-dependent -- TODO this is the exact size, some reserve should be added -paddedHeaderLen :: Int -paddedHeaderLen = 2284 +paddedHeaderLen :: VersionE2E -> Int +paddedHeaderLen v + | v >= pqRatchetE2EEncryptVersion = 2284 + | otherwise = 88 -- only used in tests to validate correct padding -- (2 bytes - version size, 1 byte - header size, not to have it fixed or version-dependent) -fullHeaderLen :: Int -fullHeaderLen = 2 + 1 + paddedHeaderLen + authTagSize + ivSize @AES256 +fullHeaderLen :: VersionE2E -> Int +fullHeaderLen v = 2 + 1 + paddedHeaderLen v + authTagSize + ivSize @AES256 -instance AlgorithmI a => Encoding (MsgHeader a) where - smpEncode MsgHeader {msgMaxVersion, msgDHRs, msgKEM, msgPN, msgNs} - | msgMaxVersion >= pqRatchetVersion = smpEncode (msgMaxVersion, msgDHRs, msgKEM, msgPN, msgNs) - | otherwise = smpEncode (msgMaxVersion, msgDHRs, msgPN, msgNs) - smpP = do - msgMaxVersion <- smpP - msgDHRs <- smpP - msgKEM <- if msgMaxVersion >= pqRatchetVersion then smpP else pure Nothing - msgPN <- smpP - msgNs <- smpP - pure MsgHeader {msgMaxVersion, msgDHRs, msgKEM, msgPN, msgNs} +-- pass the current version, as MsgHeader only includes the max supported version that can be different from the current +encodeMsgHeader :: AlgorithmI a => VersionE2E -> MsgHeader a -> ByteString +encodeMsgHeader v MsgHeader {msgMaxVersion, msgDHRs, msgKEM, msgPN, msgNs} + | v >= pqRatchetE2EEncryptVersion = smpEncode (msgMaxVersion, msgDHRs, msgKEM, msgPN, msgNs) + | otherwise = smpEncode (msgMaxVersion, msgDHRs, msgPN, msgNs) + +-- pass the current version, as MsgHeader only includes the max supported version that can be different from the current +msgHeaderP :: AlgorithmI a => VersionE2E -> Parser (MsgHeader a) +msgHeaderP v = do + msgMaxVersion <- smpP + msgDHRs <- smpP + msgKEM <- if v >= pqRatchetE2EEncryptVersion then smpP else pure Nothing + msgPN <- smpP + msgNs <- smpP + pure MsgHeader {msgMaxVersion, msgDHRs, msgKEM, msgPN, msgNs} data EncMessageHeader = EncMessageHeader - { ehVersion :: VersionE2E, + { ehVersion :: VersionE2E, -- this is current ratchet version ehIV :: IV, ehAuthTag :: AuthTag, ehBody :: ByteString } +-- this encoding depends on version in EncMessageHeader because it is "current" ratchet version instance Encoding EncMessageHeader where smpEncode EncMessageHeader {ehVersion, ehIV, ehAuthTag, ehBody} - | ehVersion >= pqRatchetVersion = smpEncode (ehVersion, ehIV, ehAuthTag, Large ehBody) + | ehVersion >= pqRatchetE2EEncryptVersion = smpEncode (ehVersion, ehIV, ehAuthTag, Large ehBody) | otherwise = smpEncode (ehVersion, ehIV, ehAuthTag, ehBody) smpP = do (ehVersion, ehIV, ehAuthTag) <- smpP - ehBody <- if ehVersion >= pqRatchetVersion then unLarge <$> smpP else smpP + ehBody <- if ehVersion >= pqRatchetE2EEncryptVersion then unLarge <$> smpP else smpP pure EncMessageHeader {ehVersion, ehIV, ehAuthTag, ehBody} +-- the header is length-prefixed to parse it as string and use as part of associated data for authenticated encryption data EncRatchetMessage = EncRatchetMessage { emHeader :: ByteString, emAuthTag :: AuthTag, emBody :: ByteString } +-- the encoder always uses 2-byte lengths for the new version, even for short headers without PQ keys. encodeEncRatchetMessage :: VersionE2E -> EncRatchetMessage -> ByteString encodeEncRatchetMessage v EncRatchetMessage {emHeader, emBody, emAuthTag} - | v >= pqRatchetVersion = smpEncode (Large emHeader, emAuthTag, Tail emBody) + | v >= pqRatchetE2EEncryptVersion = smpEncode (Large emHeader, emAuthTag, Tail emBody) | otherwise = smpEncode (emHeader, emAuthTag, Tail emBody) -encRatchetMessageP :: VersionE2E -> Parser EncRatchetMessage -encRatchetMessageP v = do - emHeader <- if v >= pqRatchetVersion then unLarge <$> smpP else smpP +-- This parser relies on the fact that header cannot be shorter than 32 bytes (it is ~69 bytes without PQ KEM), +-- therefore if the first byte is less or equal to 31 (x1F), then we have 2 byte-length limited to 8191. +-- This allows upgrading the current version in one message. +encRatchetMessageP :: Parser EncRatchetMessage +encRatchetMessageP = do + len1 <- peekWord8' + emHeader <- if len1 < 32 then unLarge <$> smpP else smpP (emAuthTag, Tail emBody) <- smpP pure EncRatchetMessage {emHeader, emBody, emAuthTag} @@ -724,6 +745,7 @@ instance ToJSON PQEncryption where instance FromJSON PQEncryption where parseJSON v = PQEncryption <$> parseJSON v + omittedField = Just PQEncOff replyKEM_ :: PQEncryption -> Maybe (RKEMParams 'RKSProposed) -> Maybe AUseKEM replyKEM_ pqEnc kem_ = case pqEnc of @@ -747,6 +769,12 @@ instance StrEncoding PQEncryption where data InitialKeys = IKUsePQ | IKNoPQ PQEncryption deriving (Eq, Show) +pattern IKPQOn :: InitialKeys +pattern IKPQOn = IKNoPQ PQEncOn + +pattern IKPQOff :: InitialKeys +pattern IKPQOff = IKNoPQ PQEncOff + instance StrEncoding InitialKeys where strEncode = \case IKUsePQ -> "pq=invitation" @@ -773,25 +801,30 @@ joinContactInitialKeys = \case rcEncrypt :: AlgorithmI a => Ratchet a -> Int -> ByteString -> Maybe PQEncryption -> ExceptT CryptoError IO (ByteString, Ratchet a) rcEncrypt Ratchet {rcSnd = Nothing} _ _ _ = throwE CERatchetState -rcEncrypt rc@Ratchet {rcSnd = Just sr@SndRatchet {rcCKs, rcHKs}, rcDHRs, rcKEM, rcNs, rcPN, rcAD = Str rcAD, rcVersion} paddedMsgLen msg pqMode_ = do +rcEncrypt rc@Ratchet {rcSnd = Just sr@SndRatchet {rcCKs, rcHKs}, rcDHRs, rcKEM, rcNs, rcPN, rcAD = Str rcAD, rcVersion} paddedMsgLen msg pqEnc_ = do -- state.CKs, mk = KDF_CK(state.CKs) let (ck', mk, iv, ehIV) = chainKdf rcCKs -- enc_header = HENCRYPT(state.HKs, header) - (ehAuthTag, ehBody) <- encryptAEAD rcHKs ehIV paddedHeaderLen rcAD msgHeader + let v = current rcVersion + (ehAuthTag, ehBody) <- encryptAEAD rcHKs ehIV (paddedHeaderLen v) rcAD (msgHeader v) -- return enc_header, ENCRYPT(mk, plaintext, CONCAT(AD, enc_header)) - -- TODO PQ versioning in Ratchet should change: we should use "current" version here - let emHeader = smpEncode EncMessageHeader {ehVersion = maxSupported rcVersion, ehBody, ehAuthTag, ehIV} + let emHeader = smpEncode EncMessageHeader {ehVersion = v, ehBody, ehAuthTag, ehIV} (emAuthTag, emBody) <- encryptAEAD mk iv paddedMsgLen (rcAD <> emHeader) msg - let msg' = encodeEncRatchetMessage (maxSupported rcVersion) EncRatchetMessage {emHeader, emBody, emAuthTag} + let msg' = encodeEncRatchetMessage v EncRatchetMessage {emHeader, emBody, emAuthTag} -- state.Ns += 1 - rc' = rc {rcSnd = Just sr {rcCKs = ck'}, rcNs = rcNs + 1} - rc'' = case pqMode_ of + -- TODO v5.8 remove comments below + -- Note that maxSupported will not downgrade here below current. + -- TODO v5.7 remove comments below + -- It will downgrade when decrypting the message when the current version downgrades to remove support for PQ encryption. + -- TODO v5.8 replace `max v currentE2EEncryptVersion` with `v` (to allow downgrade when app downgraded) + rc' = rc {rcSnd = Just sr {rcCKs = ck'}, rcNs = rcNs + 1, rcVersion = rcVersion {maxSupported = max v currentE2EEncryptVersion}} + rc'' = case pqEnc_ of Nothing -> rc' - Just rcEnableKEM - | enablePQ rcEnableKEM -> rc' {rcEnableKEM} - | otherwise -> - let rcKEM' = (\rck -> rck {rcKEMs = Nothing}) <$> rcKEM - in rc' {rcEnableKEM, rcKEM = rcKEM'} + -- This sets max version to support PQ encryption. + -- Current version upgrade happens when peer decrypts the message. + -- TODO v5.7 remove version upgrade here, as it's already upgraded above + Just PQEncOn -> rc' {rcEnableKEM = PQEncOn, rcVersion = rcVersion {maxSupported = pqRatchetE2EEncryptVersion}} + Just PQEncOff -> rc' {rcEnableKEM = PQEncOff, rcKEM = (\rck -> rck {rcKEMs = Nothing}) <$> rcKEM} pure (msg', rc'') where -- header = HEADER_PQ2( @@ -801,8 +834,9 @@ rcEncrypt rc@Ratchet {rcSnd = Just sr@SndRatchet {rcCKs, rcHKs}, rcDHRs, rcKEM, -- pn = state.PN, -- n = state.Ns -- ) - msgHeader = - smpEncode + msgHeader v = + encodeMsgHeader + v MsgHeader { msgMaxVersion = maxSupported rcVersion, msgDHRs = publicKey rcDHRs, @@ -837,7 +871,7 @@ rcDecrypt :: ExceptT CryptoError IO (DecryptResult a) rcDecrypt g rc@Ratchet {rcRcv, rcAD = Str rcAD, rcVersion} rcMKSkipped msg' = do -- TODO PQ versioning should change - encMsg@EncRatchetMessage {emHeader} <- parseE CryptoHeaderError (encRatchetMessageP $ maxSupported rcVersion) msg' + encMsg@EncRatchetMessage {emHeader} <- parseE CryptoHeaderError encRatchetMessageP msg' encHdr <- parseE CryptoHeaderError smpP emHeader -- plaintext = TrySkippedMessageKeysHE(state, enc_header, cipher-text, AD) decryptSkipped encHdr encMsg >>= \case @@ -851,9 +885,16 @@ rcDecrypt g rc@Ratchet {rcRcv, rcAD = Str rcAD, rcVersion} rcMKSkipped msg' = do SMMessage r -> pure r where decryptRcMessage :: RatchetStep -> MsgHeader a -> EncRatchetMessage -> ExceptT CryptoError IO (DecryptResult a) - decryptRcMessage rcStep MsgHeader {msgDHRs, msgKEM, msgPN, msgNs} encMsg = do + decryptRcMessage rcStep hdr@MsgHeader {msgMaxVersion, msgPN, msgNs} encMsg = do -- if dh_ratchet: - (rc', smks1) <- ratchetStep rcStep + (rc', smks1) <- case rcStep of + SameRatchet -> pure (upgradedRatchet, M.empty) + AdvanceRatchet -> do + -- SkipMessageKeysHE(state, header.pn) + (rc', hmks) <- liftEither $ skipMessageKeys msgPN upgradedRatchet + -- DHRatchetPQ2HE(state, header) + (,hmks) <$> ratchetStep rc' hdr + -- SkipMessageKeysHE(state, header.n) case skipMessageKeys msgNs rc' of Left e -> pure (Left e, rc', smkDiff smks1) Right (rc''@Ratchet {rcRcv = Just rr@RcvRatchet {rcCKr}, rcNr}, smks2) -> do @@ -863,47 +904,44 @@ rcDecrypt g rc@Ratchet {rcRcv, rcAD = Str rcAD, rcVersion} rcMKSkipped msg' = do msg <- decryptMessage (MessageKey mk iv) encMsg -- state . Nr += 1 pure (msg, rc'' {rcRcv = Just rr {rcCKr = rcCKr'}, rcNr = rcNr + 1}, smkDiff $ smks1 <> smks2) - Right (rc'', smks2) -> do + Right (rc'', smks2) -> pure (Left CERatchetState, rc'', smkDiff $ smks1 <> smks2) where + upgradedRatchet :: Ratchet a + upgradedRatchet + | msgMaxVersion > current rcVersion = rc {rcVersion = rcVersion {current = min msgMaxVersion $ maxSupported rcVersion}} + | otherwise = rc smkDiff :: SkippedMsgKeys -> SkippedMsgDiff smkDiff smks = if M.null smks then SMDNoChange else SMDAdd smks - ratchetStep :: RatchetStep -> ExceptT CryptoError IO (Ratchet a, SkippedMsgKeys) - ratchetStep SameRatchet = pure (rc, M.empty) - ratchetStep AdvanceRatchet = - -- SkipMessageKeysHE(state, header.pn) - case skipMessageKeys msgPN rc of - Left e -> throwE e - Right (rc'@Ratchet {rcDHRs, rcRK, rcNHKs, rcNHKr}, hmks) -> do - -- DHRatchetPQ2HE(state, header) - (kemSS, kemSS', rcKEM') <- pqRatchetStep rc' msgKEM - -- state.DHRs = GENERATE_DH() - (_, rcDHRs') <- atomically $ generateKeyPair @a g - -- state.RK, state.CKr, state.NHKr = KDF_RK_HE(state.RK, DH(state.DHRs, state.DHRr) || ss) - let (rcRK', rcCKr', rcNHKr') = rootKdf rcRK msgDHRs rcDHRs kemSS - -- state.RK, state.CKs, state.NHKs = KDF_RK_HE(state.RK, DH(state.DHRs, state.DHRr) || state.PQRss) - (rcRK'', rcCKs', rcNHKs') = rootKdf rcRK' msgDHRs rcDHRs' kemSS' - sndKEM = isJust kemSS' - rcvKEM = isJust kemSS - rc'' = - rc' - { rcDHRs = rcDHRs', - rcKEM = rcKEM', - rcEnableKEM = PQEncryption $ sndKEM || rcvKEM, - rcSndKEM = PQEncryption sndKEM, - rcRcvKEM = PQEncryption rcvKEM, - rcRK = rcRK'', - rcSnd = Just SndRatchet {rcDHRr = msgDHRs, rcCKs = rcCKs', rcHKs = rcNHKs}, - rcRcv = Just RcvRatchet {rcCKr = rcCKr', rcHKr = rcNHKr}, - rcPN = rcNs rc, - rcNs = 0, - rcNr = 0, - rcNHKs = rcNHKs', - rcNHKr = rcNHKr' - } - pure (rc'', hmks) + ratchetStep :: Ratchet a -> MsgHeader a -> ExceptT CryptoError IO (Ratchet a) + ratchetStep rc'@Ratchet {rcDHRs, rcRK, rcNHKs, rcNHKr} MsgHeader {msgDHRs, msgKEM} = do + (kemSS, kemSS', rcKEM') <- pqRatchetStep rc' msgKEM + -- state.DHRs = GENERATE_DH() + (_, rcDHRs') <- atomically $ generateKeyPair @a g + -- state.RK, state.CKr, state.NHKr = KDF_RK_HE(state.RK, DH(state.DHRs, state.DHRr) || ss) + let (rcRK', rcCKr', rcNHKr') = rootKdf rcRK msgDHRs rcDHRs kemSS + -- state.RK, state.CKs, state.NHKs = KDF_RK_HE(state.RK, DH(state.DHRs, state.DHRr) || state.PQRss) + (rcRK'', rcCKs', rcNHKs') = rootKdf rcRK' msgDHRs rcDHRs' kemSS' + sndKEM = isJust kemSS' + rcvKEM = isJust kemSS + pure + rc' + { rcDHRs = rcDHRs', + rcKEM = rcKEM', + rcEnableKEM = PQEncryption $ sndKEM || rcvKEM, + rcSndKEM = PQEncryption sndKEM, + rcRcvKEM = PQEncryption rcvKEM, + rcRK = rcRK'', + rcSnd = Just SndRatchet {rcDHRr = msgDHRs, rcCKs = rcCKs', rcHKs = rcNHKs}, + rcRcv = Just RcvRatchet {rcCKr = rcCKr', rcHKr = rcNHKr}, + rcPN = rcNs rc, + rcNs = 0, + rcNr = 0, + rcNHKs = rcNHKs', + rcNHKr = rcNHKr' + } pqRatchetStep :: Ratchet a -> Maybe ARKEMParams -> ExceptT CryptoError IO (Maybe KEMSharedKey, Maybe KEMSharedKey, Maybe RatchetKEM) - pqRatchetStep Ratchet {rcKEM, rcEnableKEM = PQEncryption pqEnc} = \case + pqRatchetStep Ratchet {rcKEM, rcEnableKEM = PQEncryption pqEnc, rcVersion = rv} = \case -- received message does not have KEM in header, -- but the user enabled KEM when sending previous message Nothing -> case rcKEM of @@ -913,7 +951,7 @@ rcDecrypt g rc@Ratchet {rcRcv, rcAD = Str rcAD, rcVersion} rcMKSkipped msg' = do _ -> pure (Nothing, Nothing, Nothing) -- received message has KEM in header. Just (ARKP _ ps) - | pqEnc -> do + | pqEnc && current rv >= pqRatchetE2EEncryptVersion -> do -- state.PQRr = header.kem (ss, rcPQRr) <- sharedSecret -- state.PQRct = PQKEM-ENC(state.PQRr, state.PQRss) // encapsulated additional shared secret KEM #1 @@ -981,9 +1019,9 @@ rcDecrypt g rc@Ratchet {rcRcv, rcAD = Str rcAD, rcVersion} rcMKSkipped msg' = do e -> throwE e -- header = HDECRYPT(state.NHKr, enc_header) decryptNextHeader hdr = (AdvanceRatchet,) <$> decryptHeader (rcNHKr rc) hdr - decryptHeader k EncMessageHeader {ehBody, ehAuthTag, ehIV} = do + decryptHeader k EncMessageHeader {ehVersion, ehBody, ehAuthTag, ehIV} = do header <- decryptAEAD k ehIV rcAD ehBody ehAuthTag `catchE` \_ -> throwE CERatchetHeader - parseE' CryptoHeaderError smpP header + parseE' CryptoHeaderError (msgHeaderP ehVersion) header decryptMessage :: MessageKey -> EncRatchetMessage -> ExceptT CryptoError IO (Either CryptoError ByteString) decryptMessage (MessageKey mk iv) EncRatchetMessage {emHeader, emBody, emAuthTag} = -- DECRYPT(mk, cipher-text, CONCAT(AD, enc_header)) diff --git a/tests/AgentTests.hs b/tests/AgentTests.hs index 09e9e5002..6ff9db523 100644 --- a/tests/AgentTests.hs +++ b/tests/AgentTests.hs @@ -29,7 +29,7 @@ import SMPAgentClient import SMPClient (testKeyHash, testPort, testPort2, testStoreLogFile, withSmpServer, withSmpServerStoreLogOn) import Simplex.Messaging.Agent.Protocol hiding (MID) import qualified Simplex.Messaging.Agent.Protocol as A -import Simplex.Messaging.Crypto.Ratchet (InitialKeys (..), PQEncryption (..), pattern PQEncOn, pattern PQEncOff) +import Simplex.Messaging.Crypto.Ratchet (InitialKeys (..), PQEncryption (..), pattern IKPQOn, pattern IKPQOff, pattern PQEncOn, pattern PQEncOff) import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Encoding.String import Simplex.Messaging.Protocol (ErrorType (..)) @@ -184,10 +184,10 @@ pqMatrix2NoInv = pqMatrix2_ False pqMatrix2_ :: Bool -> PQMatrix2 c pqMatrix2_ pqInv _ smpTest test = do - it "dh/dh handshake" $ smpTest $ \a b -> test (a, ikPQOff) (b, PQEncOff) - it "dh/pq handshake" $ smpTest $ \a b -> test (a, ikPQOff) (b, PQEncOn) - it "pq/dh handshake" $ smpTest $ \a b -> test (a, ikPQOn) (b, PQEncOff) - it "pq/pq handshake" $ smpTest $ \a b -> test (a, ikPQOn) (b, PQEncOn) + it "dh/dh handshake" $ smpTest $ \a b -> test (a, IKPQOff) (b, PQEncOff) + it "dh/pq handshake" $ smpTest $ \a b -> test (a, IKPQOff) (b, PQEncOn) + it "pq/dh handshake" $ smpTest $ \a b -> test (a, IKPQOn) (b, PQEncOff) + it "pq/pq handshake" $ smpTest $ \a b -> test (a, IKPQOn) (b, PQEncOn) when pqInv $ do it "pq-inv/dh handshake" $ smpTest $ \a b -> test (a, IKUsePQ) (b, PQEncOff) it "pq-inv/pq handshake" $ smpTest $ \a b -> test (a, IKUsePQ) (b, PQEncOn) @@ -199,17 +199,17 @@ pqMatrix3 :: (HasCallStack => (c, InitialKeys) -> (c, PQEncryption) -> (c, PQEncryption) -> IO ()) -> Spec pqMatrix3 _ smpTest test = do - it "dh" $ smpTest $ \a b c -> test (a, ikPQOff) (b, PQEncOff) (c, PQEncOff) - it "dh/dh/pq" $ smpTest $ \a b c -> test (a, ikPQOff) (b, PQEncOff) (c, PQEncOn) - it "dh/pq/dh" $ smpTest $ \a b c -> test (a, ikPQOff) (b, PQEncOn) (c, PQEncOff) - it "dh/pq/pq" $ smpTest $ \a b c -> test (a, ikPQOff) (b, PQEncOn) (c, PQEncOn) - it "pq/dh/dh" $ smpTest $ \a b c -> test (a, ikPQOn) (b, PQEncOff) (c, PQEncOff) - it "pq/dh/pq" $ smpTest $ \a b c -> test (a, ikPQOn) (b, PQEncOff) (c, PQEncOn) - it "pq/pq/dh" $ smpTest $ \a b c -> test (a, ikPQOn) (b, PQEncOn) (c, PQEncOff) - it "pq" $ smpTest $ \a b c -> test (a, ikPQOn) (b, PQEncOn) (c, PQEncOn) + it "dh" $ smpTest $ \a b c -> test (a, IKPQOff) (b, PQEncOff) (c, PQEncOff) + it "dh/dh/pq" $ smpTest $ \a b c -> test (a, IKPQOff) (b, PQEncOff) (c, PQEncOn) + it "dh/pq/dh" $ smpTest $ \a b c -> test (a, IKPQOff) (b, PQEncOn) (c, PQEncOff) + it "dh/pq/pq" $ smpTest $ \a b c -> test (a, IKPQOff) (b, PQEncOn) (c, PQEncOn) + it "pq/dh/dh" $ smpTest $ \a b c -> test (a, IKPQOn) (b, PQEncOff) (c, PQEncOff) + it "pq/dh/pq" $ smpTest $ \a b c -> test (a, IKPQOn) (b, PQEncOff) (c, PQEncOn) + it "pq/pq/dh" $ smpTest $ \a b c -> test (a, IKPQOn) (b, PQEncOn) (c, PQEncOff) + it "pq" $ smpTest $ \a b c -> test (a, IKPQOn) (b, PQEncOn) (c, PQEncOn) testDuplexConnection :: (HasCallStack, Transport c) => TProxy c -> c -> c -> IO () -testDuplexConnection _ alice bob = testDuplexConnection' (alice, ikPQOn) (bob, PQEncOn) +testDuplexConnection _ alice bob = testDuplexConnection' (alice, IKPQOn) (bob, PQEncOn) testDuplexConnection' :: (HasCallStack, Transport c) => (c, InitialKeys) -> (c, PQEncryption) -> IO () testDuplexConnection' (alice, aPQ) (bob, bPQ) = do @@ -246,7 +246,7 @@ testDuplexConnection' (alice, aPQ) (bob, bPQ) = do alice #:# "nothing else should be delivered to alice" testDuplexConnRandomIds :: (HasCallStack, Transport c) => TProxy c -> c -> c -> IO () -testDuplexConnRandomIds _ alice bob = testDuplexConnRandomIds' (alice, ikPQOn) (bob, PQEncOn) +testDuplexConnRandomIds _ alice bob = testDuplexConnRandomIds' (alice, IKPQOn) (bob, PQEncOn) testDuplexConnRandomIds' :: (HasCallStack, Transport c) => (c, InitialKeys) -> (c, PQEncryption) -> IO () testDuplexConnRandomIds' (alice, aPQ) (bob, bPQ) = do @@ -546,14 +546,8 @@ testResumeDeliveryQuotaExceeded _ alice bob = do -- message 8 is skipped because of alice agent sending "QCONT" message bob #: ("5", "alice", "ACK 9") #> ("5", "alice", OK) -ikPQOn :: InitialKeys -ikPQOn = IKNoPQ PQEncOn - -ikPQOff :: InitialKeys -ikPQOff = IKNoPQ PQEncOff - connect :: Transport c => (c, ByteString) -> (c, ByteString) -> IO () -connect (h1, name1) (h2, name2) = connect' (h1, name1, ikPQOn) (h2, name2, PQEncOn) +connect (h1, name1) (h2, name2) = connect' (h1, name1, IKPQOn) (h2, name2, PQEncOn) connect' :: forall c. Transport c => (c, ByteString, InitialKeys) -> (c, ByteString, PQEncryption) -> IO () connect' (h1, name1, pqMode1) (h2, name2, pqMode2) = do diff --git a/tests/AgentTests/ConnectionRequestTests.hs b/tests/AgentTests/ConnectionRequestTests.hs index 758c2d66d..8228ae4cd 100644 --- a/tests/AgentTests/ConnectionRequestTests.hs +++ b/tests/AgentTests/ConnectionRequestTests.hs @@ -81,7 +81,7 @@ testE2ERatchetParams :: RcvE2ERatchetParamsUri 'C.X448 testE2ERatchetParams = E2ERatchetParamsUri (mkVersionRange (VersionE2E 1) (VersionE2E 1)) testDhPubKey testDhPubKey Nothing testE2ERatchetParams12 :: RcvE2ERatchetParamsUri 'C.X448 -testE2ERatchetParams12 = E2ERatchetParamsUri supportedE2EEncryptVRange testDhPubKey testDhPubKey Nothing +testE2ERatchetParams12 = E2ERatchetParamsUri (supportedE2EEncryptVRange PQEncOn) testDhPubKey testDhPubKey Nothing connectionRequest :: AConnectionRequestUri connectionRequest = @@ -95,7 +95,7 @@ connectionRequestCurrentRange :: AConnectionRequestUri connectionRequestCurrentRange = ACR SCMInvitation $ CRInvitationUri - connReqData {crAgentVRange = supportedSMPAgentVRange, crSmpQueues = [queueV1, queueV1]} + connReqData {crAgentVRange = supportedSMPAgentVRange PQEncOn, crSmpQueues = [queueV1, queueV1]} testE2ERatchetParams12 connectionRequestClientDataEmpty :: AConnectionRequestUri @@ -135,7 +135,7 @@ connectionRequestTests = <> urlEncode True testDhKeyStrUri <> "&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D" strEncode connectionRequestCurrentRange - `shouldBe` "simplex:/invitation#/?v=2-4&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1%26dh%3D" + `shouldBe` "simplex:/invitation#/?v=2-5&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1%26dh%3D" <> urlEncode True testDhKeyStrUri <> "%2Csmp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1%26dh%3D" <> urlEncode True testDhKeyStrUri @@ -185,7 +185,7 @@ connectionRequestTests = <> testDhKeyStrUri <> "&e2e=extra_key%3Dnew%26v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D" <> "&some_new_param=abc" - <> "&v=2-4" + <> "&v=2-5" ) `shouldBe` Right connectionRequestCurrentRange strDecode diff --git a/tests/AgentTests/DoubleRatchetTests.hs b/tests/AgentTests/DoubleRatchetTests.hs index 87c9801ee..0e4b9079e 100644 --- a/tests/AgentTests/DoubleRatchetTests.hs +++ b/tests/AgentTests/DoubleRatchetTests.hs @@ -39,12 +39,13 @@ doubleRatchetTests = do describe "double-ratchet encryption/decryption" $ do it "should serialize and parse message header" $ do testAlgs $ testMessageHeader kdfX3DHE2EEncryptVersion - testAlgs $ testMessageHeader $ max pqRatchetVersion currentE2EEncryptVersion + testAlgs $ testMessageHeader $ max pqRatchetE2EEncryptVersion currentE2EEncryptVersion describe "message tests" $ runMessageTests initRatchets False it "should encode/decode ratchet as JSON" $ do testAlgs testKeyJSON testAlgs testRatchetJSON testVersionJSON + it "should decode v2 Ratchet with default field values" $ testDecodeV2RatchetJSON it "should agree the same ratchet parameters" $ testAlgs testX3dh it "should agree the same ratchet parameters with version 1" $ testAlgs testX3dhV1 describe "post-quantum hybrid KEM double-ratchet algorithm" $ do @@ -89,15 +90,15 @@ paddedMsgLen :: Int paddedMsgLen = 100 fullMsgLen :: VersionE2E -> Int -fullMsgLen v = headerLenLength + fullHeaderLen + C.authTagSize + paddedMsgLen +fullMsgLen v = headerLenLength + fullHeaderLen v + C.authTagSize + paddedMsgLen where - headerLenLength = if v < pqRatchetVersion then 1 else 3 -- two bytes are added because of two Large used in new encoding + headerLenLength = if v < pqRatchetE2EEncryptVersion then 1 else 3 -- two bytes are added because of two Large used in new encoding testMessageHeader :: forall a. AlgorithmI a => VersionE2E -> C.SAlgorithm a -> Expectation testMessageHeader v _ = do (k, _) <- atomically . C.generateKeyPair @a =<< C.newRandom let hdr = MsgHeader {msgMaxVersion = v, msgDHRs = k, msgKEM = Nothing, msgPN = 0, msgNs = 0} - parseAll (smpP @(MsgHeader a)) (smpEncode hdr) `shouldBe` Right hdr + parseAll (msgHeaderP v) (encodeMsgHeader v hdr) `shouldBe` Right hdr testKEMParams :: Expectation testKEMParams = do @@ -115,15 +116,15 @@ testMessageHeaderKEM _ = do g <- C.newRandom (k, _) <- atomically $ C.generateKeyPair @a g (kem, _) <- sntrup761Keypair g - let msgMaxVersion = max pqRatchetVersion currentE2EEncryptVersion + let msgMaxVersion = max pqRatchetE2EEncryptVersion currentE2EEncryptVersion msgKEM = Just . ARKP SRKSProposed $ RKParamsProposed kem hdr = MsgHeader {msgMaxVersion, msgDHRs = k, msgKEM, msgPN = 0, msgNs = 0} - parseAll (smpP @(MsgHeader a)) (smpEncode hdr) `shouldBe` Right hdr + parseAll (msgHeaderP msgMaxVersion) (encodeMsgHeader msgMaxVersion hdr) `shouldBe` Right hdr (kem', _) <- sntrup761Keypair g (ct, _) <- sntrup761Enc g kem let msgKEM' = Just . ARKP SRKSAccepted $ RKParamsAccepted ct kem' hdr' = MsgHeader {msgMaxVersion, msgDHRs = k, msgKEM = msgKEM', msgPN = 0, msgNs = 0} - parseAll (smpP @(MsgHeader a)) (smpEncode hdr') `shouldBe` Right hdr' + parseAll (msgHeaderP msgMaxVersion) (encodeMsgHeader msgMaxVersion hdr') `shouldBe` Right hdr' pattern Decrypted :: ByteString -> Either CryptoError (Either CryptoError ByteString) pattern Decrypted msg <- Right (Right msg) @@ -350,6 +351,14 @@ testVersionJSON = do testDecodeRV :: ToJSON a => a -> Expectation testDecodeRV a = J.eitherDecode' (J.encode a) `shouldBe` Right (rv 1 2) +testDecodeV2RatchetJSON :: IO () +testDecodeV2RatchetJSON = do + let v2RatchetJSON = "{\"rcVersion\":[2,2],\"rcAD\":\"2GEJrq48TmQse6NR16I-hrI0tSySZQ57E_g46nDceAPRAiF6j0drq26RTE7be6X7uiB4RaGJGf4QRXzcYuVtWw==\",\"rcDHRs\":\"TUM0Q0FRQXdCUVlESzJWdUJDSUVJRkNYbUxtSHQ3SUNfeHpGTi1Qb3ZqTVQ3S2p6XzZlZlBjOG9fRFY2RWxKOQ==\",\"rcRK\":\"BOX2X7YW5qDSp2XknY_lqacSrtDqQNPvS6iJlZIs3G0=\",\"rcNs\":0,\"rcNr\":0,\"rcPN\":0,\"rcNHKs\":\"IMouSkXUvzT_mo0WM-pqEUK09-HTLk9WOTCFQglyQxU=\",\"rcNHKr\":\"g-tus1clYPV0rGlzkf5a959tUqDYQVZ1FpcPeXdKwxI=\"}" + Right (r :: Ratchet X25519) <- pure $ J.eitherDecodeStrict' v2RatchetJSON + rcEnableKEM r `shouldBe` PQEncOff + rcSndKEM r `shouldBe` PQEncOff + rcRcvKEM r `shouldBe` PQEncOff + testEncodeDecode :: (Eq a, Show a, ToJSON a, FromJSON a) => a -> Expectation testEncodeDecode x = do let j = J.encode x @@ -359,7 +368,7 @@ testEncodeDecode x = do testX3dh :: forall a. (AlgorithmI a, DhAlgorithm a) => C.SAlgorithm a -> IO () testX3dh _ = do g <- C.newRandom - let v = max pqRatchetVersion currentE2EEncryptVersion + let v = max pqRatchetE2EEncryptVersion currentE2EEncryptVersion (pkBob1, pkBob2, Nothing, AE2ERatchetParams _ e2eBob) <- liftIO $ generateSndE2EParams @a g v Nothing (pkAlice1, pkAlice2, Nothing, e2eAlice) <- liftIO $ generateRcvE2EParams @a g v PQEncOff let paramsBob = pqX3dhSnd pkBob1 pkBob2 Nothing e2eAlice @@ -378,7 +387,7 @@ testX3dhV1 _ = do testPqX3dhProposeInReply :: forall a. (AlgorithmI a, DhAlgorithm a) => C.SAlgorithm a -> IO () testPqX3dhProposeInReply _ = do g <- C.newRandom - let v = max pqRatchetVersion currentE2EEncryptVersion + let v = max pqRatchetE2EEncryptVersion currentE2EEncryptVersion -- initiate (no KEM) (pkAlice1, pkAlice2, Nothing, e2eAlice) <- liftIO $ generateRcvE2EParams @a g v PQEncOff -- propose KEM in reply @@ -390,7 +399,7 @@ testPqX3dhProposeInReply _ = do testPqX3dhProposeAccept :: forall a. (AlgorithmI a, DhAlgorithm a) => C.SAlgorithm a -> IO () testPqX3dhProposeAccept _ = do g <- C.newRandom - let v = max pqRatchetVersion currentE2EEncryptVersion + let v = max pqRatchetE2EEncryptVersion currentE2EEncryptVersion -- initiate (propose KEM) (pkAlice1, pkAlice2, pKemAlice_@(Just _), e2eAlice) <- liftIO $ generateRcvE2EParams @a g v PQEncOn E2ERatchetParams _ _ _ (Just (RKParamsProposed aliceKem)) <- pure e2eAlice @@ -403,7 +412,7 @@ testPqX3dhProposeAccept _ = do testPqX3dhProposeReject :: forall a. (AlgorithmI a, DhAlgorithm a) => C.SAlgorithm a -> IO () testPqX3dhProposeReject _ = do g <- C.newRandom - let v = max pqRatchetVersion currentE2EEncryptVersion + let v = max pqRatchetE2EEncryptVersion currentE2EEncryptVersion -- initiate (propose KEM) (pkAlice1, pkAlice2, pKemAlice_@(Just _), e2eAlice) <- liftIO $ generateRcvE2EParams @a g v PQEncOn E2ERatchetParams _ _ _ (Just (RKParamsProposed _)) <- pure e2eAlice @@ -416,7 +425,7 @@ testPqX3dhProposeReject _ = do testPqX3dhAcceptWithoutProposalError :: forall a. (AlgorithmI a, DhAlgorithm a) => C.SAlgorithm a -> IO () testPqX3dhAcceptWithoutProposalError _ = do g <- C.newRandom - let v = max pqRatchetVersion currentE2EEncryptVersion + let v = max pqRatchetE2EEncryptVersion currentE2EEncryptVersion -- initiate (no KEM) (pkAlice1, pkAlice2, Nothing, e2eAlice) <- liftIO $ generateRcvE2EParams @a g v PQEncOff E2ERatchetParams _ _ _ Nothing <- pure e2eAlice @@ -430,7 +439,7 @@ testPqX3dhAcceptWithoutProposalError _ = do testPqX3dhProposeAgain :: forall a. (AlgorithmI a, DhAlgorithm a) => C.SAlgorithm a -> IO () testPqX3dhProposeAgain _ = do g <- C.newRandom - let v = max pqRatchetVersion currentE2EEncryptVersion + let v = max pqRatchetE2EEncryptVersion currentE2EEncryptVersion -- initiate (propose KEM) (pkAlice1, pkAlice2, pKemAlice_@(Just _), e2eAlice) <- liftIO $ generateRcvE2EParams @a g v PQEncOn E2ERatchetParams _ _ _ (Just (RKParamsProposed _)) <- pure e2eAlice @@ -452,9 +461,9 @@ compatibleRatchets _ -> expectationFailure "RatchetInitParams params are not compatible" encryptDecrypt :: (AlgorithmI a, DhAlgorithm a) => Maybe PQEncryption -> (Ratchet a -> ()) -> (Ratchet a -> ()) -> EncryptDecryptSpec a -encryptDecrypt pqEnc invalidSnd invalidRcv (alice, msg) bob = do - Right msg' <- withTVar (encrypt_ pqEnc) invalidSnd alice msg - Decrypted msg'' <- decrypt' invalidRcv bob msg' +encryptDecrypt pqEnc validSnd validRcv (alice, msg) bob = do + Right msg' <- withTVar (encrypt_ pqEnc) validSnd alice msg + Decrypted msg'' <- decrypt' validRcv bob msg' msg'' `shouldBe` msg -- enable KEM (currently disabled) @@ -493,20 +502,21 @@ withRatchets_ initRatchets_ test = do initRatchets :: (AlgorithmI a, DhAlgorithm a) => IO (Ratchet a, Ratchet a, Encrypt a, Decrypt a, EncryptDecryptSpec a) initRatchets = do g <- C.newRandom - let v = max pqRatchetVersion currentE2EEncryptVersion + let v = max pqRatchetE2EEncryptVersion currentE2EEncryptVersion (pkBob1, pkBob2, _pKemParams@Nothing, AE2ERatchetParams _ e2eBob) <- liftIO $ generateSndE2EParams g v Nothing (pkAlice1, pkAlice2, _pKem@Nothing, e2eAlice) <- liftIO $ generateRcvE2EParams g v PQEncOff Right paramsBob <- pure $ pqX3dhSnd pkBob1 pkBob2 Nothing e2eAlice Right paramsAlice <- runExceptT $ pqX3dhRcv pkAlice1 pkAlice2 Nothing e2eBob (_, pkBob3) <- atomically $ C.generateKeyPair g - let bob = initSndRatchet supportedE2EEncryptVRange (C.publicKey pkAlice2) pkBob3 paramsBob - alice = initRcvRatchet supportedE2EEncryptVRange pkAlice2 paramsAlice PQEncOff + let vs = testRatchetVersions PQEncOff + bob = initSndRatchet vs (C.publicKey pkAlice2) pkBob3 paramsBob + alice = initRcvRatchet vs pkAlice2 paramsAlice PQEncOff pure (alice, bob, encrypt' noSndKEM, decrypt' noRcvKEM, (\#>)) initRatchetsKEMProposed :: forall a. (AlgorithmI a, DhAlgorithm a) => IO (Ratchet a, Ratchet a, Encrypt a, Decrypt a, EncryptDecryptSpec a) initRatchetsKEMProposed = do g <- C.newRandom - let v = max pqRatchetVersion currentE2EEncryptVersion + let v = max pqRatchetE2EEncryptVersion currentE2EEncryptVersion -- initiate (no KEM) (pkAlice1, pkAlice2, Nothing, e2eAlice) <- liftIO $ generateRcvE2EParams g v PQEncOff -- propose KEM in reply @@ -515,14 +525,15 @@ initRatchetsKEMProposed = do Right paramsBob <- pure $ pqX3dhSnd pkBob1 pkBob2 pKemParams_ e2eAlice Right paramsAlice <- runExceptT $ pqX3dhRcv pkAlice1 pkAlice2 Nothing e2eBob (_, pkBob3) <- atomically $ C.generateKeyPair g - let bob = initSndRatchet supportedE2EEncryptVRange (C.publicKey pkAlice2) pkBob3 paramsBob - alice = initRcvRatchet supportedE2EEncryptVRange pkAlice2 paramsAlice PQEncOn + let vs = testRatchetVersions PQEncOn + bob = initSndRatchet vs (C.publicKey pkAlice2) pkBob3 paramsBob + alice = initRcvRatchet vs pkAlice2 paramsAlice PQEncOn pure (alice, bob, encrypt' hasSndKEM, decrypt' hasRcvKEM, (!#>)) initRatchetsKEMAccepted :: forall a. (AlgorithmI a, DhAlgorithm a) => IO (Ratchet a, Ratchet a, Encrypt a, Decrypt a, EncryptDecryptSpec a) initRatchetsKEMAccepted = do g <- C.newRandom - let v = max pqRatchetVersion currentE2EEncryptVersion + let v = max pqRatchetE2EEncryptVersion currentE2EEncryptVersion -- initiate (propose) (pkAlice1, pkAlice2, pKem_@(Just _), e2eAlice) <- liftIO $ generateRcvE2EParams g v PQEncOn E2ERatchetParams _ _ _ (Just (RKParamsProposed aliceKem)) <- pure e2eAlice @@ -532,14 +543,15 @@ initRatchetsKEMAccepted = do Right paramsBob <- pure $ pqX3dhSnd pkBob1 pkBob2 pKemParams_ e2eAlice Right paramsAlice <- runExceptT $ pqX3dhRcv pkAlice1 pkAlice2 pKem_ e2eBob (_, pkBob3) <- atomically $ C.generateKeyPair g - let bob = initSndRatchet supportedE2EEncryptVRange (C.publicKey pkAlice2) pkBob3 paramsBob - alice = initRcvRatchet supportedE2EEncryptVRange pkAlice2 paramsAlice PQEncOn + let vs = testRatchetVersions PQEncOn + bob = initSndRatchet vs (C.publicKey pkAlice2) pkBob3 paramsBob + alice = initRcvRatchet vs pkAlice2 paramsAlice PQEncOn pure (alice, bob, encrypt' hasSndKEM, decrypt' hasRcvKEM, (!#>)) initRatchetsKEMProposedAgain :: forall a. (AlgorithmI a, DhAlgorithm a) => IO (Ratchet a, Ratchet a, Encrypt a, Decrypt a, EncryptDecryptSpec a) initRatchetsKEMProposedAgain = do g <- C.newRandom - let v = max pqRatchetVersion currentE2EEncryptVersion + let v = max pqRatchetE2EEncryptVersion currentE2EEncryptVersion -- initiate (propose KEM) (pkAlice1, pkAlice2, pKem_@(Just _), e2eAlice) <- liftIO $ generateRcvE2EParams g v PQEncOn -- propose KEM again in reply @@ -548,10 +560,16 @@ initRatchetsKEMProposedAgain = do Right paramsBob <- pure $ pqX3dhSnd pkBob1 pkBob2 pKemParams_ e2eAlice Right paramsAlice <- runExceptT $ pqX3dhRcv pkAlice1 pkAlice2 pKem_ e2eBob (_, pkBob3) <- atomically $ C.generateKeyPair g - let bob = initSndRatchet supportedE2EEncryptVRange (C.publicKey pkAlice2) pkBob3 paramsBob - alice = initRcvRatchet supportedE2EEncryptVRange pkAlice2 paramsAlice PQEncOn + let vs = testRatchetVersions PQEncOn + bob = initSndRatchet vs (C.publicKey pkAlice2) pkBob3 paramsBob + alice = initRcvRatchet vs pkAlice2 paramsAlice PQEncOn pure (alice, bob, encrypt' hasSndKEM, decrypt' hasRcvKEM, (!#>)) +testRatchetVersions :: PQEncryption -> RatchetVersions +testRatchetVersions pq = + let v = maxVersion $ supportedE2EEncryptVRange pq + in RVersions v v + encrypt_ :: AlgorithmI a => Maybe PQEncryption -> (TVar ChaChaDRG, Ratchet a, SkippedMsgKeys) -> ByteString -> IO (Either CryptoError (ByteString, Ratchet a, SkippedMsgDiff)) encrypt_ enableKem (_, rc, _) msg = -- print msg >> @@ -559,7 +577,7 @@ encrypt_ enableKem (_, rc, _) msg = >>= either (pure . Left) checkLength where checkLength (msg', rc') = do - B.length msg' `shouldBe` fullMsgLen (maxSupported $ rcVersion rc) + B.length msg' `shouldBe` fullMsgLen (current $ rcVersion rc) pure $ Right (msg', rc', SMDNoChange) decrypt_ :: (AlgorithmI a, DhAlgorithm a) => (TVar ChaChaDRG, Ratchet a, SkippedMsgKeys) -> ByteString -> IO (Either CryptoError (Either CryptoError ByteString, Ratchet a, SkippedMsgDiff)) diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index 11adc2d0a..bc328bbad 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -63,11 +63,11 @@ import qualified Database.SQLite.Simple as SQL import SMPAgentClient import SMPClient (cfg, testPort, testPort2, testStoreLogFile2, withSmpServer, withSmpServerConfigOn, withSmpServerOn, withSmpServerStoreLogOn, withSmpServerStoreMsgLogOn, withSmpServerV7) import Simplex.Messaging.Agent hiding (createConnection, joinConnection, sendMessage) -import qualified Simplex.Messaging.Agent as Agent +import qualified Simplex.Messaging.Agent as A import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..)) import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore) import Simplex.Messaging.Agent.Protocol hiding (CON) -import qualified Simplex.Messaging.Agent.Protocol as Agent +import qualified Simplex.Messaging.Agent.Protocol as A import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), SQLiteStore (dbNew)) import Simplex.Messaging.Agent.Store.SQLite.Common (withTransaction') import Simplex.Messaging.Client (NetworkConfig (..), ProtocolClientConfig (..), TransportSessionMode (TSMEntity, TSMUser), defaultSMPClientConfig) @@ -145,7 +145,7 @@ pGet c = do _ -> pure t pattern CON :: ACommand 'Agent 'AEConn -pattern CON = Agent.CON PQEncOn +pattern CON = A.CON PQEncOn pattern Msg :: MsgBody -> ACommand 'Agent e pattern Msg msgBody <- MSG MsgMeta {integrity = MsgOk, pqEncryption = PQEncOn} _ msgBody @@ -168,13 +168,14 @@ smpCfgV7 = (smpCfg agentCfg) {serverVRange = V.mkVersionRange batchCmdsSMPVersio ntfCfgV2 :: ProtocolClientConfig NTFVersion ntfCfgV2 = (smpCfg agentCfg) {serverVRange = V.mkVersionRange (VersionNTF 1) authBatchCmdsNTFVersion} +-- TODO PQ test next version with PQ agentCfgVPrev :: AgentConfig agentCfgVPrev = agentCfg { sndAuthAlg = C.AuthAlg C.SEd25519, - smpAgentVRange = prevRange $ smpAgentVRange agentCfg, + smpAgentVRange = \_ -> prevRange $ smpAgentVRange agentCfg PQEncOff, smpClientVRange = prevRange $ smpClientVRange agentCfg, - e2eEncryptVRange = prevRange $ e2eEncryptVRange agentCfg, + e2eEncryptVRange = \_ -> prevRange $ e2eEncryptVRange agentCfg PQEncOff, smpCfg = smpCfgVPrev } @@ -187,7 +188,7 @@ agentCfgV7 = } agentCfgRatchetVPrev :: AgentConfig -agentCfgRatchetVPrev = agentCfg {e2eEncryptVRange = prevRange $ e2eEncryptVRange agentCfg} +agentCfgRatchetVPrev = agentCfg {e2eEncryptVRange = \_ -> prevRange $ e2eEncryptVRange agentCfg PQEncOff} prevRange :: VersionRange v -> VersionRange v prevRange vr = vr {maxVersion = max (minVersion vr) (prevVersion $ maxVersion vr)} @@ -223,14 +224,14 @@ inAnyOrder g rs = do expected r rp = rp r createConnection :: AgentErrorMonad m => AgentClient -> UserId -> Bool -> SConnectionMode c -> Maybe CRClientData -> SubscriptionMode -> m (ConnId, ConnectionRequestUri c) -createConnection c userId enableNtfs cMode clientData = Agent.createConnection c userId enableNtfs cMode clientData (IKNoPQ PQEncOn) +createConnection c userId enableNtfs cMode clientData = A.createConnection c userId enableNtfs cMode clientData (IKNoPQ PQEncOn) joinConnection :: AgentErrorMonad m => AgentClient -> UserId -> Bool -> ConnectionRequestUri c -> ConnInfo -> SubscriptionMode -> m ConnId -joinConnection c userId enableNtfs cReq connInfo = Agent.joinConnection c userId enableNtfs cReq connInfo PQEncOn +joinConnection c userId enableNtfs cReq connInfo = A.joinConnection c userId enableNtfs cReq connInfo PQEncOn sendMessage :: AgentErrorMonad m => AgentClient -> ConnId -> SMP.MsgFlags -> MsgBody -> m AgentMsgId sendMessage c connId msgFlags msgBody = do - (msgId, pqEnc) <- Agent.sendMessage c connId PQEncOn msgFlags msgBody + (msgId, pqEnc) <- A.sendMessage c connId PQEncOn msgFlags msgBody liftIO $ pqEnc `shouldBe` PQEncOn pure msgId @@ -267,6 +268,7 @@ functionalAPITests t = do testIncreaseConnAgentVersionMaxCompatible t it "should increase when connection was negotiated on different versions" $ testIncreaseConnAgentVersionStartDifferentVersion t + -- TODO PQ tests for upgrading connection to PQ encryption it "should deliver message after client restart" $ testDeliverClientRestart t it "should deliver messages to the user once, even if repeat delivery is made by the server (no ACK)" $ @@ -424,29 +426,25 @@ canCreateQueue allowNew (srvAuth, srvVersion) (clntAuth, clntVersion) = let v = basicAuthSMPVersion in allowNew && (isNothing srvAuth || (srvVersion >= v && clntVersion >= v && srvAuth == clntAuth)) -testMatrix2 :: ATransport -> (AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec +-- TODO PQ test next version with PQ +testMatrix2 :: ATransport -> (PQEncryption -> AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec testMatrix2 t runTest = do - it "v7" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfgV7 3 runTest - it "v7 to current" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfg 3 runTest - it "current to v7" $ withSmpServerV7 t $ runTestCfg2 agentCfg agentCfgV7 3 runTest - it "current with v7 server" $ withSmpServerV7 t $ runTestCfg2 agentCfg agentCfg 3 runTest - it "current" $ withSmpServer t $ runTestCfg2 agentCfg agentCfg 3 runTest - skip "TODO PQ versioning" $ describe "TODO fails with previous version" $ do - it "prev" $ withSmpServer t $ runTestCfg2 agentCfgVPrev agentCfgVPrev 3 runTest - it "prev to current" $ withSmpServer t $ runTestCfg2 agentCfgVPrev agentCfg 3 runTest - it "current to prev" $ withSmpServer t $ runTestCfg2 agentCfg agentCfgVPrev 3 runTest + it "v7" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfgV7 3 $ runTest PQEncOn + it "v7 to current" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfg 3 $ runTest PQEncOn + it "current to v7" $ withSmpServerV7 t $ runTestCfg2 agentCfg agentCfgV7 3 $ runTest PQEncOn + it "current with v7 server" $ withSmpServerV7 t $ runTestCfg2 agentCfg agentCfg 3 $ runTest PQEncOn + it "current" $ withSmpServer t $ runTestCfg2 agentCfg agentCfg 3 $ runTest PQEncOn + it "prev" $ withSmpServer t $ runTestCfg2 agentCfgVPrev agentCfgVPrev 3 $ runTest PQEncOff + it "prev to current" $ withSmpServer t $ runTestCfg2 agentCfgVPrev agentCfg 3 $ runTest PQEncOff + it "current to prev" $ withSmpServer t $ runTestCfg2 agentCfg agentCfgVPrev 3 $ runTest PQEncOff -testRatchetMatrix2 :: ATransport -> (AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec +-- TODO PQ test next version with PQ +testRatchetMatrix2 :: ATransport -> (PQEncryption -> AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec testRatchetMatrix2 t runTest = do - it "ratchet current" $ withSmpServer t $ runTestCfg2 agentCfg agentCfg 3 runTest - skip "TODO PQ versioning" $ describe "TODO fails with previous version" $ do - pendingV "ratchet prev" $ withSmpServer t $ runTestCfg2 agentCfgRatchetVPrev agentCfgRatchetVPrev 3 runTest - pendingV "ratchets prev to current" $ withSmpServer t $ runTestCfg2 agentCfgRatchetVPrev agentCfg 3 runTest - pendingV "ratchets current to prev" $ withSmpServer t $ runTestCfg2 agentCfg agentCfgRatchetVPrev 3 runTest - where - pendingV d = - let vr = e2eEncryptVRange agentCfg - in if minVersion vr == maxVersion vr then skip "previous version is not supported" . it d else it d + it "ratchet current" $ withSmpServer t $ runTestCfg2 agentCfg agentCfg 3 $ runTest PQEncOn + it "ratchet prev" $ withSmpServer t $ runTestCfg2 agentCfgRatchetVPrev agentCfgRatchetVPrev 3 $ runTest PQEncOff + it "ratchets prev to current" $ withSmpServer t $ runTestCfg2 agentCfgRatchetVPrev agentCfg 3 $ runTest PQEncOff + it "ratchets current to prev" $ withSmpServer t $ runTestCfg2 agentCfg agentCfgRatchetVPrev 3 $ runTest PQEncOff testServerMatrix2 :: ATransport -> (InitialAgentServers -> IO ()) -> Spec testServerMatrix2 t runTest = do @@ -468,40 +466,40 @@ withAgentClientsCfg2 aCfg bCfg runTest = do withAgentClients2 :: (AgentClient -> AgentClient -> IO ()) -> IO () withAgentClients2 = withAgentClientsCfg2 agentCfg agentCfg -runAgentClientTest :: HasCallStack => AgentClient -> AgentClient -> AgentMsgId -> IO () -runAgentClientTest alice@AgentClient {} bob baseId = +runAgentClientTest :: HasCallStack => PQEncryption -> AgentClient -> AgentClient -> AgentMsgId -> IO () +runAgentClientTest pqEnc alice@AgentClient {} bob baseId = runRight_ $ do - (bobId, qInfo) <- createConnection alice 1 True SCMInvitation Nothing SMSubscribe - aliceId <- joinConnection bob 1 True qInfo "bob's connInfo" SMSubscribe + (bobId, qInfo) <- A.createConnection alice 1 True SCMInvitation Nothing (IKNoPQ pqEnc) SMSubscribe + aliceId <- A.joinConnection bob 1 True qInfo "bob's connInfo" pqEnc SMSubscribe ("", _, CONF confId _ "bob's connInfo") <- get alice allowConnection alice bobId confId "alice's connInfo" - get alice ##> ("", bobId, CON) + get alice ##> ("", bobId, A.CON pqEnc) get bob ##> ("", aliceId, INFO "alice's connInfo") - get bob ##> ("", aliceId, CON) + get bob ##> ("", aliceId, A.CON pqEnc) -- message IDs 1 to 3 (or 1 to 4 in v1) get assigned to control messages, so first MSG is assigned ID 4 - 1 <- msgId <$> sendMessage alice bobId SMP.noMsgFlags "hello" + 1 <- msgId <$> A.sendMessage alice bobId pqEnc SMP.noMsgFlags "hello" get alice ##> ("", bobId, SENT $ baseId + 1) - 2 <- msgId <$> sendMessage alice bobId SMP.noMsgFlags "how are you?" + 2 <- msgId <$> A.sendMessage alice bobId pqEnc SMP.noMsgFlags "how are you?" get alice ##> ("", bobId, SENT $ baseId + 2) - get bob =##> \case ("", c, Msg "hello") -> c == aliceId; _ -> False + get bob =##> \case ("", c, Msg' _ pq "hello") -> c == aliceId && pq == pqEnc; _ -> False ackMessage bob aliceId (baseId + 1) Nothing - get bob =##> \case ("", c, Msg "how are you?") -> c == aliceId; _ -> False + get bob =##> \case ("", c, Msg' _ pq "how are you?") -> c == aliceId && pq == pqEnc; _ -> False ackMessage bob aliceId (baseId + 2) Nothing - 3 <- msgId <$> sendMessage bob aliceId SMP.noMsgFlags "hello too" + 3 <- msgId <$> A.sendMessage bob aliceId pqEnc SMP.noMsgFlags "hello too" get bob ##> ("", aliceId, SENT $ baseId + 3) - 4 <- msgId <$> sendMessage bob aliceId SMP.noMsgFlags "message 1" + 4 <- msgId <$> A.sendMessage bob aliceId pqEnc SMP.noMsgFlags "message 1" get bob ##> ("", aliceId, SENT $ baseId + 4) - get alice =##> \case ("", c, Msg "hello too") -> c == bobId; _ -> False + get alice =##> \case ("", c, Msg' _ pq "hello too") -> c == bobId && pq == pqEnc; _ -> False ackMessage alice bobId (baseId + 3) Nothing - get alice =##> \case ("", c, Msg "message 1") -> c == bobId; _ -> False + get alice =##> \case ("", c, Msg' _ pq "message 1") -> c == bobId && pq == pqEnc; _ -> False ackMessage alice bobId (baseId + 4) Nothing suspendConnection alice bobId - 5 <- msgId <$> sendMessage bob aliceId SMP.noMsgFlags "message 2" + 5 <- msgId <$> A.sendMessage bob aliceId pqEnc SMP.noMsgFlags "message 2" get bob ##> ("", aliceId, MERR (baseId + 5) (SMP AUTH)) deleteConnection alice bobId liftIO $ noMessages alice "nothing else should be delivered to alice" where - msgId = subtract baseId + msgId = subtract baseId . fst testAgentClient3 :: HasCallStack => IO () testAgentClient3 = do @@ -529,42 +527,42 @@ testAgentClient3 = do get c =##> \case ("", connId, Msg "c5") -> connId == aIdForC; _ -> False ackMessage c aIdForC 5 Nothing -runAgentClientContactTest :: HasCallStack => AgentClient -> AgentClient -> AgentMsgId -> IO () -runAgentClientContactTest alice bob baseId = +runAgentClientContactTest :: HasCallStack => PQEncryption -> AgentClient -> AgentClient -> AgentMsgId -> IO () +runAgentClientContactTest pqEnc alice bob baseId = runRight_ $ do - (_, qInfo) <- createConnection alice 1 True SCMContact Nothing SMSubscribe - aliceId <- joinConnection bob 1 True qInfo "bob's connInfo" SMSubscribe + (_, qInfo) <- A.createConnection alice 1 True SCMContact Nothing (IKNoPQ pqEnc) SMSubscribe + aliceId <- A.joinConnection bob 1 True qInfo "bob's connInfo" pqEnc SMSubscribe ("", _, REQ invId _ "bob's connInfo") <- get alice bobId <- acceptContact alice True invId "alice's connInfo" PQEncOn SMSubscribe ("", _, CONF confId _ "alice's connInfo") <- get bob allowConnection bob aliceId confId "bob's connInfo" get alice ##> ("", bobId, INFO "bob's connInfo") - get alice ##> ("", bobId, CON) - get bob ##> ("", aliceId, CON) + get alice ##> ("", bobId, A.CON pqEnc) + get bob ##> ("", aliceId, A.CON pqEnc) -- message IDs 1 to 3 (or 1 to 4 in v1) get assigned to control messages, so first MSG is assigned ID 4 - 1 <- msgId <$> sendMessage alice bobId SMP.noMsgFlags "hello" + 1 <- msgId <$> A.sendMessage alice bobId pqEnc SMP.noMsgFlags "hello" get alice ##> ("", bobId, SENT $ baseId + 1) - 2 <- msgId <$> sendMessage alice bobId SMP.noMsgFlags "how are you?" + 2 <- msgId <$> A.sendMessage alice bobId pqEnc SMP.noMsgFlags "how are you?" get alice ##> ("", bobId, SENT $ baseId + 2) - get bob =##> \case ("", c, Msg "hello") -> c == aliceId; _ -> False + get bob =##> \case ("", c, Msg' _ pq "hello") -> c == aliceId && pq == pqEnc; _ -> False ackMessage bob aliceId (baseId + 1) Nothing - get bob =##> \case ("", c, Msg "how are you?") -> c == aliceId; _ -> False + get bob =##> \case ("", c, Msg' _ pq "how are you?") -> c == aliceId && pq == pqEnc; _ -> False ackMessage bob aliceId (baseId + 2) Nothing - 3 <- msgId <$> sendMessage bob aliceId SMP.noMsgFlags "hello too" + 3 <- msgId <$> A.sendMessage bob aliceId pqEnc SMP.noMsgFlags "hello too" get bob ##> ("", aliceId, SENT $ baseId + 3) - 4 <- msgId <$> sendMessage bob aliceId SMP.noMsgFlags "message 1" + 4 <- msgId <$> A.sendMessage bob aliceId pqEnc SMP.noMsgFlags "message 1" get bob ##> ("", aliceId, SENT $ baseId + 4) - get alice =##> \case ("", c, Msg "hello too") -> c == bobId; _ -> False + get alice =##> \case ("", c, Msg' _ pq "hello too") -> c == bobId && pq == pqEnc; _ -> False ackMessage alice bobId (baseId + 3) Nothing - get alice =##> \case ("", c, Msg "message 1") -> c == bobId; _ -> False + get alice =##> \case ("", c, Msg' _ pq "message 1") -> c == bobId && pq == pqEnc; _ -> False ackMessage alice bobId (baseId + 4) Nothing suspendConnection alice bobId - 5 <- msgId <$> sendMessage bob aliceId SMP.noMsgFlags "message 2" + 5 <- msgId <$> A.sendMessage bob aliceId pqEnc SMP.noMsgFlags "message 2" get bob ##> ("", aliceId, MERR (baseId + 5) (SMP AUTH)) deleteConnection alice bobId liftIO $ noMessages alice "nothing else should be delivered to alice" where - msgId = subtract baseId + msgId = subtract baseId . fst noMessages :: HasCallStack => AgentClient -> String -> Expectation noMessages c err = tryGet `shouldReturn` () @@ -688,12 +686,12 @@ testAllowConnectionClientRestart t = do testIncreaseConnAgentVersion :: HasCallStack => ATransport -> IO () testIncreaseConnAgentVersion t = do - alice <- getSMPAgentClient' 1 agentCfg {smpAgentVRange = mkVersionRange 1 2} initAgentServers testDB - bob <- getSMPAgentClient' 2 agentCfg {smpAgentVRange = mkVersionRange 1 2} initAgentServers testDB2 + alice <- getSMPAgentClient' 1 agentCfg {smpAgentVRange = \_ -> mkVersionRange 1 2} initAgentServers testDB + bob <- getSMPAgentClient' 2 agentCfg {smpAgentVRange = \_ -> mkVersionRange 1 2} initAgentServers testDB2 withSmpServerStoreMsgLogOn t testPort $ \_ -> do (aliceId, bobId) <- runRight $ do - (aliceId, bobId) <- makeConnection alice bob - exchangeGreetingsMsgId 4 alice bobId bob aliceId + (aliceId, bobId) <- makeConnection_ PQEncOff alice bob + exchangeGreetingsMsgId_ PQEncOff 4 alice bobId bob aliceId checkVersion alice bobId 2 checkVersion bob aliceId 2 pure (aliceId, bobId) @@ -701,42 +699,42 @@ testIncreaseConnAgentVersion t = do -- version doesn't increase if incompatible disconnectAgentClient alice - alice2 <- getSMPAgentClient' 3 agentCfg {smpAgentVRange = mkVersionRange 1 3} initAgentServers testDB + alice2 <- getSMPAgentClient' 3 agentCfg {smpAgentVRange = \_ -> mkVersionRange 1 3} initAgentServers testDB runRight_ $ do subscribeConnection alice2 bobId - exchangeGreetingsMsgId 6 alice2 bobId bob aliceId + exchangeGreetingsMsgId_ PQEncOff 6 alice2 bobId bob aliceId checkVersion alice2 bobId 2 checkVersion bob aliceId 2 -- version increases if compatible disconnectAgentClient bob - bob2 <- getSMPAgentClient' 4 agentCfg {smpAgentVRange = mkVersionRange 1 3} initAgentServers testDB2 + bob2 <- getSMPAgentClient' 4 agentCfg {smpAgentVRange = \_ -> mkVersionRange 1 3} initAgentServers testDB2 runRight_ $ do subscribeConnection bob2 aliceId - exchangeGreetingsMsgId 8 alice2 bobId bob2 aliceId + exchangeGreetingsMsgId_ PQEncOff 8 alice2 bobId bob2 aliceId checkVersion alice2 bobId 3 checkVersion bob2 aliceId 3 -- version doesn't decrease, even if incompatible disconnectAgentClient alice2 - alice3 <- getSMPAgentClient' 5 agentCfg {smpAgentVRange = mkVersionRange 2 2} initAgentServers testDB + alice3 <- getSMPAgentClient' 5 agentCfg {smpAgentVRange = \_ -> mkVersionRange 2 2} initAgentServers testDB runRight_ $ do subscribeConnection alice3 bobId - exchangeGreetingsMsgId 10 alice3 bobId bob2 aliceId + exchangeGreetingsMsgId_ PQEncOff 10 alice3 bobId bob2 aliceId checkVersion alice3 bobId 3 checkVersion bob2 aliceId 3 disconnectAgentClient bob2 - bob3 <- getSMPAgentClient' 6 agentCfg {smpAgentVRange = mkVersionRange 1 1} initAgentServers testDB2 + bob3 <- getSMPAgentClient' 6 agentCfg {smpAgentVRange = \_ -> mkVersionRange 1 1} initAgentServers testDB2 runRight_ $ do subscribeConnection bob3 aliceId - exchangeGreetingsMsgId 12 alice3 bobId bob3 aliceId + exchangeGreetingsMsgId_ PQEncOff 12 alice3 bobId bob3 aliceId checkVersion alice3 bobId 3 checkVersion bob3 aliceId 3 disconnectAgentClient alice3 @@ -749,12 +747,12 @@ checkVersion c connId v = do testIncreaseConnAgentVersionMaxCompatible :: HasCallStack => ATransport -> IO () testIncreaseConnAgentVersionMaxCompatible t = do - alice <- getSMPAgentClient' 1 agentCfg {smpAgentVRange = mkVersionRange 1 2} initAgentServers testDB - bob <- getSMPAgentClient' 2 agentCfg {smpAgentVRange = mkVersionRange 1 2} initAgentServers testDB2 + alice <- getSMPAgentClient' 1 agentCfg {smpAgentVRange = \_ -> mkVersionRange 1 2} initAgentServers testDB + bob <- getSMPAgentClient' 2 agentCfg {smpAgentVRange = \_ -> mkVersionRange 1 2} initAgentServers testDB2 withSmpServerStoreMsgLogOn t testPort $ \_ -> do (aliceId, bobId) <- runRight $ do - (aliceId, bobId) <- makeConnection alice bob - exchangeGreetingsMsgId 4 alice bobId bob aliceId + (aliceId, bobId) <- makeConnection_ PQEncOff alice bob + exchangeGreetingsMsgId_ PQEncOff 4 alice bobId bob aliceId checkVersion alice bobId 2 checkVersion bob aliceId 2 pure (aliceId, bobId) @@ -762,14 +760,14 @@ testIncreaseConnAgentVersionMaxCompatible t = do -- version increases to max compatible disconnectAgentClient alice - alice2 <- getSMPAgentClient' 3 agentCfg {smpAgentVRange = mkVersionRange 1 3} initAgentServers testDB + alice2 <- getSMPAgentClient' 3 agentCfg {smpAgentVRange = \_ -> mkVersionRange 1 3} initAgentServers testDB disconnectAgentClient bob - bob2 <- getSMPAgentClient' 4 agentCfg {smpAgentVRange = mkVersionRange 1 4} initAgentServers testDB2 + bob2 <- getSMPAgentClient' 4 agentCfg {smpAgentVRange = supportedSMPAgentVRange} initAgentServers testDB2 runRight_ $ do subscribeConnection alice2 bobId subscribeConnection bob2 aliceId - exchangeGreetingsMsgId 6 alice2 bobId bob2 aliceId + exchangeGreetingsMsgId_ PQEncOff 6 alice2 bobId bob2 aliceId checkVersion alice2 bobId 3 checkVersion bob2 aliceId 3 disconnectAgentClient alice2 @@ -777,12 +775,12 @@ testIncreaseConnAgentVersionMaxCompatible t = do testIncreaseConnAgentVersionStartDifferentVersion :: HasCallStack => ATransport -> IO () testIncreaseConnAgentVersionStartDifferentVersion t = do - alice <- getSMPAgentClient' 1 agentCfg {smpAgentVRange = mkVersionRange 1 2} initAgentServers testDB - bob <- getSMPAgentClient' 2 agentCfg {smpAgentVRange = mkVersionRange 1 3} initAgentServers testDB2 + alice <- getSMPAgentClient' 1 agentCfg {smpAgentVRange = \_ -> mkVersionRange 1 2} initAgentServers testDB + bob <- getSMPAgentClient' 2 agentCfg {smpAgentVRange = \_ -> mkVersionRange 1 3} initAgentServers testDB2 withSmpServerStoreMsgLogOn t testPort $ \_ -> do (aliceId, bobId) <- runRight $ do - (aliceId, bobId) <- makeConnection alice bob - exchangeGreetingsMsgId 4 alice bobId bob aliceId + (aliceId, bobId) <- makeConnection_ PQEncOff alice bob + exchangeGreetingsMsgId_ PQEncOff 4 alice bobId bob aliceId checkVersion alice bobId 2 checkVersion bob aliceId 2 pure (aliceId, bobId) @@ -790,11 +788,11 @@ testIncreaseConnAgentVersionStartDifferentVersion t = do -- version increases to max compatible disconnectAgentClient alice - alice2 <- getSMPAgentClient' 3 agentCfg {smpAgentVRange = mkVersionRange 1 3} initAgentServers testDB + alice2 <- getSMPAgentClient' 3 agentCfg {smpAgentVRange = \_ -> mkVersionRange 1 3} initAgentServers testDB runRight_ $ do subscribeConnection alice2 bobId - exchangeGreetingsMsgId 6 alice2 bobId bob aliceId + exchangeGreetingsMsgId_ PQEncOff 6 alice2 bobId bob aliceId checkVersion alice2 bobId 3 checkVersion bob aliceId 3 disconnectAgentClient alice2 @@ -1075,13 +1073,13 @@ setupDesynchronizedRatchet alice bob = do runRight_ $ do subscribeConnection bob2 aliceId - Left Agent.CMD {cmdErr = PROHIBITED} <- runExceptT $ synchronizeRatchet bob2 aliceId PQEncOn False + Left A.CMD {cmdErr = PROHIBITED} <- runExceptT $ synchronizeRatchet bob2 aliceId PQEncOn False 8 <- sendMessage alice bobId SMP.noMsgFlags "hello 5" get alice ##> ("", bobId, SENT 8) get bob2 =##> ratchetSyncP aliceId RSRequired - Left Agent.CMD {cmdErr = PROHIBITED} <- runExceptT $ sendMessage bob2 aliceId SMP.noMsgFlags "hello 6" + Left A.CMD {cmdErr = PROHIBITED} <- runExceptT $ sendMessage bob2 aliceId SMP.noMsgFlags "hello 6" pure () pure (aliceId, bobId, bob2) @@ -1265,13 +1263,13 @@ makeConnectionForUsers = makeConnectionForUsers_ PQEncOn makeConnectionForUsers_ :: PQEncryption -> AgentClient -> UserId -> AgentClient -> UserId -> ExceptT AgentErrorType IO (ConnId, ConnId) makeConnectionForUsers_ pqEnc alice aliceUserId bob bobUserId = do - (bobId, qInfo) <- Agent.createConnection alice aliceUserId True SCMInvitation Nothing (CR.IKNoPQ pqEnc) SMSubscribe - aliceId <- Agent.joinConnection bob bobUserId True qInfo "bob's connInfo" pqEnc SMSubscribe + (bobId, qInfo) <- A.createConnection alice aliceUserId True SCMInvitation Nothing (CR.IKNoPQ pqEnc) SMSubscribe + aliceId <- A.joinConnection bob bobUserId True qInfo "bob's connInfo" pqEnc SMSubscribe ("", _, CONF confId _ "bob's connInfo") <- get alice allowConnection alice bobId confId "alice's connInfo" - get alice ##> ("", bobId, Agent.CON pqEnc) + get alice ##> ("", bobId, A.CON pqEnc) get bob ##> ("", aliceId, INFO "alice's connInfo") - get bob ##> ("", aliceId, Agent.CON pqEnc) + get bob ##> ("", aliceId, A.CON pqEnc) pure (aliceId, bobId) testInactiveNoSubs :: ATransport -> IO () @@ -2032,7 +2030,7 @@ testAbortSwitchStarted servers = do liftIO $ rcvSwchStatuses' stats `shouldMatchList` [Just RSSwitchStarted] phaseRcv a bId SPStarted [Just RSSendingQADD, Nothing] -- repeat switch is prohibited - Left Agent.CMD {cmdErr = PROHIBITED} <- runExceptT $ switchConnectionAsync a "" bId + Left A.CMD {cmdErr = PROHIBITED} <- runExceptT $ switchConnectionAsync a "" bId -- abort current switch stats' <- abortConnectionSwitch a bId liftIO $ rcvSwchStatuses' stats' `shouldMatchList` [Nothing] @@ -2154,7 +2152,7 @@ testCannotAbortSwitchSecured servers = do withA' $ \a -> do phaseRcv a bId SPConfirmed [Just RSSendingQADD, Nothing] phaseRcv a bId SPSecured [Just RSSendingQUSE, Nothing] - Left Agent.CMD {cmdErr = PROHIBITED} <- runExceptT $ abortConnectionSwitch a bId + Left A.CMD {cmdErr = PROHIBITED} <- runExceptT $ abortConnectionSwitch a bId pure () withA $ \a -> withB $ \b -> runRight_ $ do subscribeConnection a bId @@ -2346,53 +2344,61 @@ testDeliveryReceipts = get a =##> \case ("", c, Msg "hello too") -> c == bId; _ -> False ackMessage a bId 6 $ Just "" get b =##> \case ("", c, Rcvd 6) -> c == aId; _ -> False - ackMessage b aId 7 (Just "") `catchError` \e -> liftIO $ e `shouldBe` Agent.CMD PROHIBITED + ackMessage b aId 7 (Just "") `catchError` \e -> liftIO $ e `shouldBe` A.CMD PROHIBITED ackMessage b aId 7 Nothing testDeliveryReceiptsVersion :: HasCallStack => ATransport -> IO () testDeliveryReceiptsVersion t = do - a <- getSMPAgentClient' 1 agentCfg {smpAgentVRange = mkVersionRange 1 3} initAgentServers testDB - b <- getSMPAgentClient' 2 agentCfg {smpAgentVRange = mkVersionRange 1 3} initAgentServers testDB2 + a <- getSMPAgentClient' 1 agentCfg {smpAgentVRange = \_ -> mkVersionRange 1 3} initAgentServers testDB + b <- getSMPAgentClient' 2 agentCfg {smpAgentVRange = \_ -> mkVersionRange 1 3} initAgentServers testDB2 withSmpServerStoreMsgLogOn t testPort $ \_ -> do (aId, bId) <- runRight $ do - (aId, bId) <- makeConnection a b + (aId, bId) <- makeConnection_ PQEncOff a b checkVersion a bId 3 checkVersion b aId 3 - 4 <- sendMessage a bId SMP.noMsgFlags "hello" + (4, _) <- A.sendMessage a bId PQEncOff SMP.noMsgFlags "hello" get a ##> ("", bId, SENT 4) - get b =##> \case ("", c, Msg "hello") -> c == aId; _ -> False + get b =##> \case ("", c, Msg' 4 PQEncOff "hello") -> c == aId; _ -> False ackMessage b aId 4 $ Just "" liftIO $ noMessages a "no delivery receipt (unsupported version)" - 5 <- sendMessage b aId SMP.noMsgFlags "hello too" + (5, _) <- A.sendMessage b aId PQEncOff SMP.noMsgFlags "hello too" get b ##> ("", aId, SENT 5) - get a =##> \case ("", c, Msg "hello too") -> c == bId; _ -> False + get a =##> \case ("", c, Msg' 5 PQEncOff "hello too") -> c == bId; _ -> False ackMessage a bId 5 $ Just "" liftIO $ noMessages b "no delivery receipt (unsupported version)" pure (aId, bId) disconnectAgentClient a disconnectAgentClient b - a' <- getSMPAgentClient' 3 agentCfg {smpAgentVRange = mkVersionRange 1 4} initAgentServers testDB - b' <- getSMPAgentClient' 4 agentCfg {smpAgentVRange = mkVersionRange 1 4} initAgentServers testDB2 + a' <- getSMPAgentClient' 3 agentCfg {smpAgentVRange = supportedSMPAgentVRange} initAgentServers testDB + b' <- getSMPAgentClient' 4 agentCfg {smpAgentVRange = supportedSMPAgentVRange} initAgentServers testDB2 runRight_ $ do subscribeConnection a' bId subscribeConnection b' aId - exchangeGreetingsMsgId 6 a' bId b' aId + exchangeGreetingsMsgId_ PQEncOff 6 a' bId b' aId checkVersion a' bId 4 checkVersion b' aId 4 - 8 <- sendMessage a' bId SMP.noMsgFlags "hello" + (8, PQEncOff) <- A.sendMessage a' bId PQEncOn SMP.noMsgFlags "hello" get a' ##> ("", bId, SENT 8) - get b' =##> \case ("", c, Msg "hello") -> c == aId; _ -> False + get b' =##> \case ("", c, Msg' 8 PQEncOff "hello") -> c == aId; _ -> False ackMessage b' aId 8 $ Just "" get a' =##> \case ("", c, Rcvd 8) -> c == bId; _ -> False ackMessage a' bId 9 Nothing - 10 <- sendMessage b' aId SMP.noMsgFlags "hello too" + (10, PQEncOff) <- A.sendMessage b' aId PQEncOn SMP.noMsgFlags "hello too" get b' ##> ("", aId, SENT 10) - get a' =##> \case ("", c, Msg "hello too") -> c == bId; _ -> False + get a' =##> \case ("", c, Msg' 10 PQEncOff "hello too") -> c == bId; _ -> False ackMessage a' bId 10 $ Just "" get b' =##> \case ("", c, Rcvd 10) -> c == aId; _ -> False ackMessage b' aId 11 Nothing + -- TODO PQ this part hangs when waiting for Rcvd, because connection tries to upgrade to PQ encryption. + -- replacing 2 PQEncOn with PQEncOff above prevents hanging. + -- (12, _) <- A.sendMessage a' bId PQEncOn SMP.noMsgFlags "hello 2" + -- get a' ##> ("", bId, SENT 12) + -- get b' =##> \case ("", c, Msg' 12 PQEncOff "hello 2") -> c == aId; _ -> False + -- ackMessage b' aId 12 $ Just "" + -- get a' =##> \case ("", c, Rcvd 12) -> c == bId; _ -> False + -- ackMessage a' bId 13 Nothing disconnectAgentClient a' disconnectAgentClient b' @@ -2562,12 +2568,12 @@ exchangeGreetingsMsgId = exchangeGreetingsMsgId_ PQEncOn exchangeGreetingsMsgId_ :: HasCallStack => PQEncryption -> Int64 -> AgentClient -> ConnId -> AgentClient -> ConnId -> ExceptT AgentErrorType IO () exchangeGreetingsMsgId_ pqEnc msgId alice bobId bob aliceId = do - msgId1 <- Agent.sendMessage alice bobId pqEnc SMP.noMsgFlags "hello" + msgId1 <- A.sendMessage alice bobId pqEnc SMP.noMsgFlags "hello" liftIO $ msgId1 `shouldBe` (msgId, pqEnc) get alice ##> ("", bobId, SENT msgId) get bob =##> \case ("", c, Msg' mId pq "hello") -> c == aliceId && mId == msgId && pq == pqEnc; _ -> False ackMessage bob aliceId msgId Nothing - msgId2 <- Agent.sendMessage bob aliceId pqEnc SMP.noMsgFlags "hello too" + msgId2 <- A.sendMessage bob aliceId pqEnc SMP.noMsgFlags "hello too" let msgId' = msgId + 1 liftIO $ msgId2 `shouldBe` (msgId', pqEnc) get bob ##> ("", aliceId, SENT msgId')