From 488398df9f3d1a9bab844f25d42170b5a2bff65a Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 5 Jan 2022 19:52:37 +0000 Subject: [PATCH] change message envelopes and encoding, unify message delivery (#252) * types and encodings for double ratchet integration * upgrade stack resolver * type classes for version agreement, encode/decode connection request links and E2E params with versioning * encode/decode client parameters (version and DH key) in SMP queue URI using query string parameters * restore support of the current SMP queue URI format * update AMessage to only send queues in REPLY message (not the full connection request) * new agent message evnvelopes (tests fail) * new message envelopes - tests pass * store fully encrypted messages before sending * unify message delivery via DB queue (excluding confirmation and invitation) * remove activateSecuredQueue * linter hints * remove comment * export order * save rachet-encrypted message, not per-queue encrypted * delete message after it is accepted by the server, reduce message delivery interval for the tests Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com> --- migrations/20210101_initial.sql | 5 +- protocol/agent-protocol.md | 31 +- protocol/simplex-messaging.md | 2 +- src/Simplex/Messaging/Agent.hs | 276 +++++++------- src/Simplex/Messaging/Agent/Client.hs | 95 ++--- src/Simplex/Messaging/Agent/Env/SQLite.hs | 7 - src/Simplex/Messaging/Agent/Protocol.hs | 381 ++++++++++++++------ src/Simplex/Messaging/Agent/Store.hs | 9 +- src/Simplex/Messaging/Agent/Store/SQLite.hs | 53 ++- src/Simplex/Messaging/Crypto.hs | 57 ++- src/Simplex/Messaging/Crypto/Ratchet.hs | 116 +++--- src/Simplex/Messaging/Encoding.hs | 45 ++- src/Simplex/Messaging/Encoding/String.hs | 13 +- src/Simplex/Messaging/Protocol.hs | 39 +- src/Simplex/Messaging/Transport.hs | 7 +- src/Simplex/Messaging/Version.hs | 95 ++++- stack.yaml | 4 +- tests/AgentTests.hs | 6 +- tests/AgentTests/ConnectionRequestTests.hs | 90 ++++- tests/AgentTests/DoubleRatchetTests.hs | 4 +- tests/AgentTests/FunctionalAPITests.hs | 6 - tests/AgentTests/SQLiteTests.hs | 44 +-- tests/CoreTests/VersionRangeTests.hs | 25 +- tests/SMPAgentClient.hs | 2 +- 24 files changed, 848 insertions(+), 564 deletions(-) diff --git a/migrations/20210101_initial.sql b/migrations/20210101_initial.sql index 595aa2b48..ce8e10ea4 100644 --- a/migrations/20210101_initial.sql +++ b/migrations/20210101_initial.sql @@ -8,7 +8,7 @@ CREATE TABLE servers ( CREATE TABLE connections ( conn_alias BLOB NOT NULL PRIMARY KEY, conn_mode TEXT NOT NULL, - last_internal_msg_id INTEGER NOT NULL DEFAULT 0, + last_internal_msg_id INTEGER NOT NULL DEFAULT -3, last_internal_rcv_msg_id INTEGER NOT NULL DEFAULT 0, last_internal_snd_msg_id INTEGER NOT NULL DEFAULT 0, last_external_snd_msg_id INTEGER NOT NULL DEFAULT 0, @@ -31,7 +31,7 @@ CREATE TABLE rcv_queues ( snd_key BLOB, status TEXT NOT NULL, smp_server_version INTEGER NOT NULL DEFAULT 1, - smp_client_version INTEGER NOT NULL DEFAULT 1, + smp_client_version INTEGER, PRIMARY KEY (host, port, rcv_id), FOREIGN KEY (host, port) REFERENCES servers ON DELETE RESTRICT ON UPDATE CASCADE, @@ -60,6 +60,7 @@ CREATE TABLE messages ( internal_ts TEXT NOT NULL, internal_rcv_id INTEGER, internal_snd_id INTEGER, + msg_type BLOB NOT NULL, -- SMP_CONF?, HELLO, REPLY, DELETE msg_body BLOB NOT NULL DEFAULT x'', PRIMARY KEY (conn_alias, internal_id), FOREIGN KEY (conn_alias, internal_rcv_id) REFERENCES rcv_messages diff --git a/protocol/agent-protocol.md b/protocol/agent-protocol.md index 922cf3a41..2da95753c 100644 --- a/protocol/agent-protocol.md +++ b/protocol/agent-protocol.md @@ -137,47 +137,28 @@ previousMsgHash = encoded encoded = agentMessage = helloMsg / replyQueueMsg / - clientMsg / invitationMsg/ acknowledgeMsg / + clientMsg / invitationMsg / newQueueMessage / deleteQueueMsg msgPadding = *OCTET ; optional random bytes to get messages to the same size (as defined in SMP message size) -helloMsg = %s"HELLO" +helloMsg = %s"H" -replyQueueMsg = %s"REPLY" SP connectionRequest ; `connectionRequest` is defined below +replyQueueMsg = %s"R" connectionRequest ; `connectionRequest` is defined below ; this message can only be sent by the second connection party -clientMsg = %s"MSG" SP clientMsgBody +clientMsg = %s"M" clientMsgBody clientMsgBody = *OCTET ; TODO remove and move to "public" header invitationMsg = %s"INV" SP connReqInvitation SP connInfo ; `connReqInvitation` and `connInfo` are defined below -acknowledgeMsg = %s"ACK" SP agentMsgId SP msgHash SP ackStatus -; NOT SUPPORTED in the current implementation - -msgHash = encoded -; base64 encoded hash of the received message - -ackStatus = %s"OK" / ackError - -ackError = %s"ERR" SP ackErrorType - -ackErrorType = ackUnknownMsg / ackProhibitedMsg / ackSyntaxErr - -ackUnknownMsg = %s"UNKNOWN" - -ackProhibitedMsg = %s"PROHIBITED" ; unexpected message e.g. "HELLO" or "REPLY" - -ackSyntaxErr = %s"SYNTAX" SP syntaxErrCode -syntaxErrCode = 1*DIGIT ; TODO - -newQueueMsg = %s"NEW" SP queueURI +newQueueMsg = %s"N" queueURI ; this message can be sent by any party to add SMP queue to the connection. ; NOT SUPPORTED in the current implementation -deleteQueueMsg = %s"DEL" SP queueURI +deleteQueueMsg = %s"D" queueURI ; notification that the queue with passed URI will be deleted ; no need to notify the other party about suspending queue separately, as suspended and deleted queues are indistinguishable to the sender ; NOT SUPPORTED in the current implementation diff --git a/protocol/simplex-messaging.md b/protocol/simplex-messaging.md index 960b5e7a7..872c23faa 100644 --- a/protocol/simplex-messaging.md +++ b/protocol/simplex-messaging.md @@ -567,7 +567,7 @@ This command is sent to the server by the sender both to confirm the queue after ```abnf send = %s"SEND " smpEncMessage smpEncMessage = smpPubHeader sentMsgBody ; message up to 15968 bytes -smpPubHeader = smpClientVersion (%x01 senderPublicDhKey / %x00) +smpPubHeader = smpClientVersion ("1" senderPublicDhKey / "0") smpClientVersion = word16 senderPublicDhKey = length x509encoded ; sender's Curve25519 public key to agree DH secret for E2E encryption in this queue diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index a75c14353..e30bc236d 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -11,7 +11,6 @@ {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} -{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TupleSections #-} @@ -90,6 +89,7 @@ import Simplex.Messaging.Protocol (MsgBody) import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Transport (ATransport (..), TProxy, Transport (..), loadTLSServerParams, runTransportServer, simplexMQVersion) import Simplex.Messaging.Util (bshow, tryError, unlessM) +import Simplex.Messaging.Version import System.Random (randomR) import UnliftIO.Async (async, race_) import qualified UnliftIO.Exception as E @@ -138,11 +138,11 @@ disconnectAgentClient c = closeAgentClient c >> logConnection c False type AgentErrorMonad m = (MonadUnliftIO m, MonadError AgentErrorType m) -- | Create SMP agent connection (NEW command) -createConnection :: AgentErrorMonad m => AgentClient -> SConnectionMode c -> m (ConnId, ConnectionRequest c) +createConnection :: AgentErrorMonad m => AgentClient -> SConnectionMode c -> m (ConnId, ConnectionRequestUri c) createConnection c cMode = withAgentEnv c $ newConn c "" cMode -- | Join SMP agent connection (JOIN command) -joinConnection :: AgentErrorMonad m => AgentClient -> ConnectionRequest c -> ConnInfo -> m ConnId +joinConnection :: AgentErrorMonad m => AgentClient -> ConnectionRequestUri c -> ConnInfo -> m ConnId joinConnection c = withAgentEnv c .: joinConn c "" -- | Allow connection to continue after CONF notification (LET command) @@ -254,8 +254,8 @@ withStore action = do -- | execute any SMP agent command processCommand :: forall m. AgentMonad m => AgentClient -> (ConnId, ACommand 'Client) -> m (ConnId, ACommand 'Agent) processCommand c (connId, cmd) = case cmd of - NEW (ACM cMode) -> second (INV . ACR cMode) <$> newConn c connId cMode - JOIN (ACR _ cReq) connInfo -> (,OK) <$> joinConn c connId cReq connInfo + NEW (ACM cMode) -> second (INV . ACRU cMode) <$> newConn c connId cMode + JOIN (ACRU _ cReq) connInfo -> (,OK) <$> joinConn c connId cReq connInfo LET confId ownCInfo -> allowConnection' c connId confId ownCInfo $> (connId, OK) ACPT invId ownCInfo -> (,OK) <$> acceptContact' c connId invId ownCInfo RJCT invId -> rejectContact' c connId invId $> (connId, OK) @@ -265,7 +265,7 @@ processCommand c (connId, cmd) = case cmd of OFF -> suspendConnection' c connId $> (connId, OK) DEL -> deleteConnection' c connId $> (connId, OK) -newConn :: AgentMonad m => AgentClient -> ConnId -> SConnectionMode c -> m (ConnId, ConnectionRequest c) +newConn :: AgentMonad m => AgentClient -> ConnId -> SConnectionMode c -> m (ConnId, ConnectionRequestUri c) newConn c connId cMode = do srv <- getSMPServer (rq, qUri) <- newRcvQueue c srv @@ -273,37 +273,39 @@ newConn c connId cMode = do let cData = ConnData {connId} connId' <- withStore $ \st -> createRcvConn st g cData rq cMode addSubscription c rq connId' - let crData = ConnReqData simplexChat [qUri] ConnectionEncryption + let crData = ConnReqUriData simplexChat smpAgentVRange [qUri] pure . (connId',) $ case cMode of - SCMInvitation -> CRInvitation crData - SCMContact -> CRContact crData + SCMInvitation -> CRInvitationUri crData connEncStubUri + SCMContact -> CRContactUri crData -joinConn :: AgentMonad m => AgentClient -> ConnId -> ConnectionRequest c -> ConnInfo -> m ConnId -joinConn c connId (CRInvitation (ConnReqData _ (qUri :| _) _)) cInfo = do - (sq, smpConf) <- newSndQueue qUri cInfo - g <- asks idsDrg - cfg <- asks config - let cData = ConnData {connId} - connId' <- withStore $ \st -> createSndConn st g cData sq - confirmQueue c sq smpConf - activateQueueJoining c connId' sq $ retryInterval cfg - pure connId' -joinConn c connId (CRContact (ConnReqData _ (qUri :| _) _)) cInfo = do +joinConn :: AgentMonad m => AgentClient -> ConnId -> ConnectionRequestUri c -> ConnInfo -> m ConnId +joinConn c connId (CRInvitationUri (ConnReqUriData _ _ (qUri :| _)) _e2eEnc) cInfo = do + -- TODO check all versions in connection request are compatible with supported + -- (add agent and e2e) + case qUri `compatibleVersion` SMP.smpClientVersion of + Nothing -> throwError $ AGENT A_VERSION + Just qInfo -> do + (sq, smpConf) <- newSndQueue qInfo cInfo + g <- asks idsDrg + let cData = ConnData {connId} + connId' <- withStore $ \st -> createSndConn st g cData sq + confirmQueue c sq smpConf + void $ enqueueMessage c connId' sq HELLO + pure connId' +joinConn c connId (CRContactUri (ConnReqUriData _ _ (qUri :| _))) cInfo = do (connId', cReq) <- newConn c connId SCMInvitation sendInvitation c qUri cReq cInfo pure connId' -activateQueueJoining :: forall m. AgentMonad m => AgentClient -> ConnId -> SndQueue -> RetryInterval -> m () -activateQueueJoining c connId sq retryInterval = - activateQueue c connId sq retryInterval createReplyQueue - where - createReplyQueue :: m () - createReplyQueue = do - srv <- getSMPServer - (rq, qUri') <- newRcvQueue c srv - addSubscription c rq connId - withStore $ \st -> upgradeSndConnToDuplex st connId rq - sendControlMessage c sq . REPLY $ CRInvitation $ ConnReqData CRSSimplex [qUri'] ConnectionEncryption +createReplyQueue :: AgentMonad m => AgentClient -> ConnId -> SndQueue -> m () +createReplyQueue c connId sq = do + srv <- getSMPServer + (rq, qUri) <- newRcvQueue c srv + -- TODO reply queue version should be the same as send queue, ignoring it in v1 + let qInfo = toVersionT qUri (maxVersion SMP.smpClientVersion) + addSubscription c rq connId + withStore $ \st -> upgradeSndConnToDuplex st connId rq + void . enqueueMessage c connId sq $ REPLY [qInfo] -- | Approve confirmation (LET command) in Reader monad allowConnection' :: AgentMonad m => AgentClient -> ConnId -> ConfirmationId -> ConnInfo -> m () @@ -342,61 +344,58 @@ subscribeConnection' c connId = withStore (`getConn` connId) >>= \case SomeConn _ (DuplexConnection _ rq sq) -> do resumeMsgDelivery c connId sq + subscribeQueue c rq connId case status (sq :: SndQueue) of Confirmed -> do + -- TODO if there is no confirmation saved, just update the status without securing the queue AcceptedConfirmation {senderConf = SMPConfirmation {senderKey}} <- withStore (`getAcceptedConfirmation` connId) secureQueue c rq senderKey withStore $ \st -> setRcvQueueStatus st rq Secured - activateSecuredQueue rq sq - Secured -> activateSecuredQueue rq sq - Active -> subscribeQueue c rq connId + Secured -> pure () + Active -> pure () _ -> throwError $ INTERNAL "unexpected queue status" SomeConn _ (SndConnection _ sq) -> do resumeMsgDelivery c connId sq case status (sq :: SndQueue) of - Confirmed -> activateQueueJoining c connId sq =<< resumeInterval + Confirmed -> pure () Active -> throwError $ CONN SIMPLEX _ -> throwError $ INTERNAL "unexpected queue status" SomeConn _ (RcvConnection _ rq) -> subscribeQueue c rq connId SomeConn _ (ContactConnection _ rq) -> subscribeQueue c rq connId - where - activateSecuredQueue :: RcvQueue -> SndQueue -> m () - activateSecuredQueue rq sq = do - activateQueueInitiating c connId sq =<< resumeInterval - subscribeQueue c rq connId - resumeInterval :: m RetryInterval - resumeInterval = do - r <- asks $ retryInterval . config - pure r {initialInterval = 5_000_000} -- | Send message to the connection (SEND command) in Reader monad sendMessage' :: forall m. AgentMonad m => AgentClient -> ConnId -> MsgBody -> m AgentMsgId sendMessage' c connId msg = withStore (`getConn` connId) >>= \case - SomeConn _ (DuplexConnection _ _ sq) -> enqueueMessage sq - SomeConn _ (SndConnection _ sq) -> enqueueMessage sq + SomeConn _ (DuplexConnection _ _ sq) -> enqueueMsg sq + SomeConn _ (SndConnection _ sq) -> enqueueMsg sq _ -> throwError $ CONN SIMPLEX where - enqueueMessage :: SndQueue -> m AgentMsgId - enqueueMessage sq@SndQueue {server} = do - resumeMsgDelivery c connId sq - msgId <- storeSentMsg - queuePendingMsgs c connId server [msgId] - pure $ unId msgId - where - storeSentMsg :: m InternalId - storeSentMsg = do - internalTs <- liftIO getCurrentTime - withStore $ \st -> do - (internalId, internalSndId, prevMsgHash) <- updateSndIds st connId - let msgBody = - serializeAgentMessage $ - AgentMessage (AHeader (unSndId internalSndId) prevMsgHash) (A_MSG msg) - internalHash = C.sha256Hash msgBody - msgData = SndMsgData {..} - createSndMsg st connId msgData - pure internalId + enqueueMsg :: SndQueue -> m AgentMsgId + enqueueMsg sq = enqueueMessage c connId sq $ A_MSG msg + +enqueueMessage :: forall m. AgentMonad m => AgentClient -> ConnId -> SndQueue -> AMessage -> m AgentMsgId +enqueueMessage c connId sq@SndQueue {server} aMessage = do + resumeMsgDelivery c connId sq + msgId <- storeSentMsg + queuePendingMsgs c connId server [msgId] + pure $ unId msgId + where + storeSentMsg :: m InternalId + storeSentMsg = do + internalTs <- liftIO getCurrentTime + (internalId, internalSndId, prevMsgHash) <- withStore (`updateSndIds` connId) + let privHeader = APrivHeader (unSndId internalSndId) prevMsgHash + agentMessage = smpEncode $ AgentMessage' privHeader aMessage + internalHash = C.sha256Hash agentMessage + + encAgentMessage <- agentRatchetEncrypt agentMessage + let msgBody = smpEncode $ AgentMsgEnvelope {agentVersion = smpAgentVersion, encAgentMessage} + msgType = aMessageType aMessage + msgData = SndMsgData {internalId, internalSndId, internalTs, msgType, msgBody, internalHash, prevMsgHash} + withStore $ \st -> createSndMsg st connId msgData + pure internalId resumeMsgDelivery :: forall m. AgentMonad m => AgentClient -> ConnId -> SndQueue -> m () resumeMsgDelivery c connId SndQueue {server} = do @@ -440,17 +439,32 @@ runSrvMsgDelivery c@AgentClient {subQ} srv = do withStore (\st -> E.try $ getPendingMsgData st connId msgId) >>= \case Left (e :: E.SomeException) -> notify connId $ MERR mId (INTERNAL $ show e) - Right (sq, msgBody) -> do + Right (sq, rq_, (msgType, msgBody)) -> do withRetryInterval ri $ \loop -> do tryError (sendAgentMessage c sq msgBody) >>= \case Left e -> case e of SMP SMP.QUOTA -> loop + SMP SMP.AUTH -> case msgType of + HELLO_ -> loop + REPLY_ -> notify connId $ ERR e + A_MSG_ -> notify connId $ MERR mId e SMP {} -> notify connId $ MERR mId e CMD {} -> notify connId $ MERR mId e _ -> loop Right () -> do - notify connId $ SENT mId - withStore $ \st -> updateSndMsgStatus st connId msgId SndMsgSent + case msgType of + HELLO_ -> do + withStore $ \st -> setSndQueueStatus st sq Active + case rq_ of + -- party initiating connection + Just rq -> do + subscribeQueue c rq connId + notify connId CON + -- party joining connection + _ -> createReplyQueue c connId sq + A_MSG_ -> notify connId $ SENT mId + _ -> pure () + withStore $ \st -> deleteMsg st connId msgId where notify :: ConnId -> ACommand 'Agent -> m () notify connId cmd = atomically $ writeTBQueue subQ ("", connId, cmd) @@ -501,11 +515,6 @@ getSMPServer = i <- atomically . stateTVar gen $ randomR (0, L.length servers - 1) pure $ servers L.!! i -sendControlMessage :: AgentMonad m => AgentClient -> SndQueue -> AMessage -> m () -sendControlMessage c sq agentMessage = do - sendAgentMessage c sq . serializeAgentMessage $ - AgentMessage (AHeader 0 "") agentMessage - subscriber :: (MonadUnliftIO m, MonadReader Env m) => AgentClient -> m () subscriber c@AgentClient {msgQ} = forever $ do t <- atomically $ readTBQueue msgQ @@ -527,31 +536,37 @@ processSMPTransmission c@AgentClient {subQ} (srv, rId, cmd) = do SMP.MSG srvMsgId srvTs msgBody' -> handleNotifyAck $ do -- TODO deduplicate with previously received msgBody <- agentCbDecrypt rcvDhSecret (C.cbNonce srvMsgId) msgBody' - encMessage@SMP.EncMessage {emHeader = SMP.PubHeader v e2ePubKey_} <- - liftEither $ parse smpP (AGENT A_MESSAGE) msgBody + clientMsg@SMP.ClientMsgEnvelope {cmHeader = SMP.PubHeader v e2ePubKey_} <- + parseMessage msgBody case (e2eDhSecret, e2ePubKey_) of (Nothing, Just e2ePubKey) -> do let e2eDh = C.dh' e2ePubKey e2ePrivKey - (_, agentMessage) <- - decryptAgentMessage e2eDh encMessage - case agentMessage of - AgentConfirmation senderKey connInfo -> do - smpConfirmation SMPConfirmation {senderKey, e2ePubKey, connInfo} - ack - AgentInvitation cReq cInfo -> smpInvitation cReq cInfo >> ack + decryptClientMessage e2eDh clientMsg >>= \case + (SMP.PHConfirmation senderKey, AgentConfirmation {agentVersion = _v, e2eEncryption, encConnInfo}) -> do + agentMsgBody <- agentRatchetDecrypt encConnInfo + agentMessage <- parseMessage agentMsgBody + case agentMessage of + AgentConnInfo connInfo -> do + smpConfirmation SMPConfirmation {senderKey, e2ePubKey, connInfo} + ack + _ -> prohibited >> ack + (SMP.PHEmpty, AgentInvitation' {agentVersion = _v, connReq, connInfo}) -> + smpInvitation connReq connInfo >> ack _ -> prohibited >> ack (Just e2eDh, Nothing) -> do - (msg, agentMessage) <- - decryptAgentMessage e2eDh encMessage - case agentMessage of - AgentMessage AHeader {sndMsgId, prevMsgHash} aMsg -> case aMsg of - HELLO -> helloMsg >> ack - REPLY cReq -> replyMsg cReq >> ack - A_MSG body -> do - -- note that there is no ACK sent here, it is sent with agent's user ACK command - -- TODO add hash to other messages - let msgHash = C.sha256Hash msg - agentClientMsg prevMsgHash sndMsgId (srvMsgId, systemToUTCTime srvTs) body msgHash + decryptClientMessage e2eDh clientMsg >>= \case + (SMP.PHEmpty, AgentMsgEnvelope _v encAgentMsg) -> do + agentMsgBody <- agentRatchetDecrypt encAgentMsg + agentMessage <- parseMessage agentMsgBody + case agentMessage of + AgentMessage' APrivHeader {sndMsgId, prevMsgHash} aMessage -> do + msgMeta <- agentClientMsg' prevMsgHash sndMsgId (srvMsgId, systemToUTCTime srvTs) agentMsgBody aMessage + case aMessage of + HELLO -> helloMsg >> ack + REPLY cReq -> replyMsg cReq >> ack + -- note that there is no ACK sent here, it is sent with agent's user ACK command + A_MSG body -> notify $ MSG msgMeta body + _ -> prohibited >> ack _ -> prohibited >> ack _ -> prohibited >> ack SMP.END -> do @@ -574,12 +589,15 @@ processSMPTransmission c@AgentClient {subQ} (srv, rId, cmd) = do ack :: m () ack = sendAck c rq - decryptAgentMessage :: C.DhSecretX25519 -> SMP.EncMessage -> m (ByteString, AgentMessage) - decryptAgentMessage e2eDh SMP.EncMessage {emNonce, emBody} = do - msg <- agentCbDecrypt e2eDh emNonce emBody - agentMessage <- - liftEither $ clientToAgentMsg =<< parse smpP (AGENT A_MESSAGE) msg - pure (msg, agentMessage) + decryptClientMessage :: C.DhSecretX25519 -> SMP.ClientMsgEnvelope -> m (SMP.PrivHeader, AgentMsgEnvelope) + decryptClientMessage e2eDh SMP.ClientMsgEnvelope {cmNonce, cmEncBody} = do + clientMsg <- agentCbDecrypt e2eDh cmNonce cmEncBody + SMP.ClientMessage privHeader clientBody <- parseMessage clientMsg + agentEnvelope <- parseMessage clientBody + pure (privHeader, agentEnvelope) + + parseMessage :: Encoding a => ByteString -> m a + parseMessage = liftEither . parse smpP (AGENT A_MESSAGE) smpConfirmation :: SMPConfirmation -> m () smpConfirmation senderConf@SMPConfirmation {connInfo} = do @@ -608,33 +626,37 @@ processSMPTransmission c@AgentClient {subQ} (srv, rId, cmd) = do SCDuplex -> notifyConnected c connId _ -> pure () - replyMsg :: ConnectionRequest 'CMInvitation -> m () - replyMsg (CRInvitation (ConnReqData _ (qUri :| _) _)) = do + replyMsg :: L.NonEmpty SMPQueueInfo -> m () + replyMsg (qInfo :| _) = do logServer "<--" c srv rId "MSG " case cType of SCRcv -> do AcceptedConfirmation {ownConnInfo} <- withStore (`getAcceptedConfirmation` connId) - (sq, smpConf) <- newSndQueue qUri ownConnInfo - withStore $ \st -> upgradeRcvConnToDuplex st connId sq - confirmQueue c sq smpConf - withStore (`removeConfirmations` connId) - cfg <- asks config - activateQueueInitiating c connId sq $ retryInterval cfg + case qInfo `proveCompatible` SMP.smpClientVersion of + Nothing -> notify (ERR $ AGENT A_VERSION) >> ack + Just qInfo' -> do + (sq, smpConf) <- newSndQueue qInfo' ownConnInfo + withStore $ \st -> upgradeRcvConnToDuplex st connId sq + confirmQueue c sq smpConf + withStore (`removeConfirmations` connId) + void $ enqueueMessage c connId sq HELLO _ -> prohibited - agentClientMsg :: PrevRcvMsgHash -> ExternalSndId -> (BrokerId, BrokerTs) -> MsgBody -> MsgHash -> m () - agentClientMsg externalPrevSndHash sndMsgId broker msgBody internalHash = do + agentClientMsg' :: PrevRcvMsgHash -> ExternalSndId -> (BrokerId, BrokerTs) -> MsgBody -> AMessage -> m MsgMeta + agentClientMsg' externalPrevSndHash sndMsgId broker msgBody aMessage = do logServer "<--" c srv rId "MSG " + let internalHash = C.sha256Hash msgBody internalTs <- liftIO getCurrentTime (internalId, internalRcvId, prevExtSndId, prevRcvMsgHash) <- withStore (`updateRcvIds` connId) let integrity = checkMsgIntegrity prevExtSndId sndMsgId prevRcvMsgHash externalPrevSndHash recipient = (unId internalId, internalTs) msgMeta = MsgMeta {integrity, recipient, broker, sndMsgId} - rcvMsg = RcvMsgData {msgMeta, msgBody, internalRcvId, internalHash, externalPrevSndHash} + msgType = aMessageType aMessage + rcvMsg = RcvMsgData {msgMeta, msgType, msgBody, internalRcvId, internalHash, externalPrevSndHash} withStore $ \st -> createRcvMsg st connId rcvMsg - notify $ MSG msgMeta msgBody + pure msgMeta - smpInvitation :: ConnectionRequest 'CMInvitation -> ConnInfo -> m () + smpInvitation :: ConnectionRequestUri 'CMInvitation -> ConnInfo -> m () smpInvitation connReq cInfo = do logServer "<--" c srv rId "MSG " case cType of @@ -656,41 +678,25 @@ processSMPTransmission c@AgentClient {subQ} (srv, rId, cmd) = do confirmQueue :: AgentMonad m => AgentClient -> SndQueue -> SMPConfirmation -> m () confirmQueue c sq smpConf = do - sendConfirmation c sq smpConf + sendConfirmation c sq smpConf connEncStub withStore $ \st -> setSndQueueStatus st sq Confirmed -activateQueueInitiating :: AgentMonad m => AgentClient -> ConnId -> SndQueue -> RetryInterval -> m () -activateQueueInitiating c connId sq retryInterval = - activateQueue c connId sq retryInterval $ notifyConnected c connId - -activateQueue :: forall m. AgentMonad m => AgentClient -> ConnId -> SndQueue -> RetryInterval -> m () -> m () -activateQueue c connId sq retryInterval afterActivation = - getActivation c connId >>= \case - Nothing -> async runActivation >>= addActivation c connId - Just _ -> pure () - where - runActivation :: m () - runActivation = do - sendHello c sq retryInterval - withStore $ \st -> setSndQueueStatus st sq Active - removeActivation c connId - afterActivation - notifyConnected :: AgentMonad m => AgentClient -> ConnId -> m () notifyConnected c connId = atomically $ writeTBQueue (subQ c) ("", connId, CON) -newSndQueue :: (MonadUnliftIO m, MonadReader Env m) => SMPQueueUri -> ConnInfo -> m (SndQueue, SMPConfirmation) -newSndQueue qUri cInfo = +newSndQueue :: (MonadUnliftIO m, MonadReader Env m) => Compatible SMPQueueInfo -> ConnInfo -> m (SndQueue, SMPConfirmation) +newSndQueue qInfo cInfo = asks (cmdSignAlg . config) >>= \case - C.SignAlg a -> newSndQueue_ a qUri cInfo + C.SignAlg a -> newSndQueue_ a qInfo cInfo newSndQueue_ :: (C.SignatureAlgorithm a, C.AlgorithmI a, MonadUnliftIO m) => C.SAlgorithm a -> - SMPQueueUri -> + Compatible SMPQueueInfo -> ConnInfo -> m (SndQueue, SMPConfirmation) -newSndQueue_ a (SMPQueueUri smpServer senderId clientVersion rcvE2ePubDhKey) cInfo = do +newSndQueue_ a (Compatible (SMPQueueInfo _clientVersion smpServer senderId rcvE2ePubDhKey)) cInfo = do + -- this function assumes clientVersion is compatible - it was tested before (senderKey, sndPrivateKey) <- liftIO $ C.generateSignatureKeyPair a (e2ePubKey, e2ePrivKey) <- liftIO C.generateKeyPair' let sndQueue = diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index e5e1a2ca7..c3f5277e0 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -4,6 +4,7 @@ {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} @@ -19,9 +20,11 @@ module Simplex.Messaging.Agent.Client sendConfirmation, sendInvitation, RetryInterval (..), - sendHello, secureQueue, sendAgentMessage, + agentRatchetEncrypt, + agentRatchetDecrypt, + agentCbEncrypt, agentCbDecrypt, sendAck, suspendQueue, @@ -40,7 +43,6 @@ import Control.Logger.Simple import Control.Monad.Except import Control.Monad.IO.Unlift import Control.Monad.Reader -import Control.Monad.Trans.Except import Data.Bifunctor (first) import Data.ByteString.Base64 import Data.ByteString.Char8 (ByteString) @@ -58,7 +60,7 @@ import Simplex.Messaging.Agent.Store import Simplex.Messaging.Client import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding -import Simplex.Messaging.Protocol (ErrorType (AUTH), MsgBody, QueueId, QueueIdsKeys (..), SndPublicVerifyKey) +import Simplex.Messaging.Protocol (MsgBody, QueueId, QueueIdsKeys (..), SndPublicVerifyKey) import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Util (bshow, liftEitherError, liftError) import Simplex.Messaging.Version @@ -304,41 +306,37 @@ showServer srv = B.pack $ host srv <> maybe "" (":" <>) (port srv) logSecret :: ByteString -> ByteString logSecret bs = encode $ B.take 3 bs -sendConfirmation :: forall m. AgentMonad m => AgentClient -> SndQueue -> SMPConfirmation -> m () -sendConfirmation c sq@SndQueue {server, sndId} SMPConfirmation {senderKey, e2ePubKey, connInfo} = +-- TODO maybe package E2ERatchetParams into SMPConfirmation +sendConfirmation :: forall m. AgentMonad m => AgentClient -> SndQueue -> SMPConfirmation -> E2ERatchetParams -> m () +sendConfirmation c sq@SndQueue {server, sndId} SMPConfirmation {senderKey, e2ePubKey, connInfo} e2eEncryption = withLogSMP_ c server sndId "SEND " $ \smp -> do msg <- mkConfirmation liftSMP $ sendSMPMessage smp Nothing sndId msg where mkConfirmation :: m MsgBody - mkConfirmation = - agentCbEncrypt sq (Just e2ePubKey) . serializeAgentMessage $ - AgentConfirmation senderKey connInfo + mkConfirmation = do + encConnInfo <- agentRatchetEncrypt . smpEncode $ AgentConnInfo connInfo + let agentEnvelope = + AgentConfirmation + { agentVersion = smpAgentVersion, + e2eEncryption, + encConnInfo + } + agentCbEncrypt sq (Just e2ePubKey) . smpEncode $ + SMP.ClientMessage (SMP.PHConfirmation senderKey) $ smpEncode agentEnvelope -sendHello :: forall m. AgentMonad m => AgentClient -> SndQueue -> RetryInterval -> m () -sendHello c sq@SndQueue {server, sndId, sndPrivateKey} ri = - withLogSMP_ c server sndId "SEND (retrying)" $ \smp -> do - msg <- mkHello - liftSMP . withRetryInterval ri $ \loop -> - sendSMPMessage smp (Just sndPrivateKey) sndId msg `catchE` \case - SMPServerError AUTH -> loop - e -> throwE e - where - mkHello :: m ByteString - mkHello = do - agentCbEncrypt sq Nothing . serializeAgentMessage $ - AgentMessage (AHeader 0 "") HELLO - -sendInvitation :: forall m. AgentMonad m => AgentClient -> SMPQueueUri -> ConnectionRequest 'CMInvitation -> ConnInfo -> m () -sendInvitation c SMPQueueUri {smpServer, senderId, dhPublicKey} cReq connInfo = do +sendInvitation :: forall m. AgentMonad m => AgentClient -> SMPQueueUri -> ConnectionRequestUri 'CMInvitation -> ConnInfo -> m () +sendInvitation c SMPQueueUri {smpServer, senderId, dhPublicKey} connReq connInfo = do withLogSMP_ c smpServer senderId "SEND " $ \smp -> do msg <- mkInvitation liftSMP $ sendSMPMessage smp Nothing senderId msg where mkInvitation :: m ByteString - mkInvitation = - agentCbEncryptOnce dhPublicKey . serializeAgentMessage $ - AgentInvitation cReq connInfo + -- this is only encrypted with per-queue E2E, not with double ratchet + mkInvitation = do + let agentEnvelope = AgentInvitation' {agentVersion = smpAgentVersion, connReq, connInfo} + agentCbEncryptOnce dhPublicKey . smpEncode $ + SMP.ClientMessage SMP.PHEmpty $ smpEncode agentEnvelope secureQueue :: AgentMonad m => AgentClient -> RcvQueue -> SndPublicVerifyKey -> m () secureQueue c RcvQueue {server, rcvId, rcvPrivateKey} senderKey = @@ -360,34 +358,47 @@ deleteQueue c RcvQueue {server, rcvId, rcvPrivateKey} = withLogSMP c server rcvId "DEL" $ \smp -> deleteSMPQueue smp rcvPrivateKey rcvId -sendAgentMessage :: AgentMonad m => AgentClient -> SndQueue -> ByteString -> m () -sendAgentMessage c sq@SndQueue {server, sndId, sndPrivateKey} msg = - withLogSMP_ c server sndId "SEND " $ \smp -> do - msg' <- agentCbEncrypt sq Nothing msg - liftSMP $ sendSMPMessage smp (Just sndPrivateKey) sndId msg' +-- TODO this is just wrong +sendAgentMessage :: forall m. AgentMonad m => AgentClient -> SndQueue -> ByteString -> m () +sendAgentMessage c sq@SndQueue {server, sndId, sndPrivateKey} agentMsg = + withLogSMP_ c server sndId "SEND " $ \smp -> do + let clientMsg = SMP.ClientMessage SMP.PHEmpty agentMsg + msg <- agentCbEncrypt sq Nothing $ smpEncode clientMsg + liftSMP $ sendSMPMessage smp (Just sndPrivateKey) sndId msg + +-- encoded AgentMessage' -> encoded EncAgentMessage +agentRatchetEncrypt :: AgentMonad m => ByteString -> m ByteString +agentRatchetEncrypt = pure + +-- encoded EncAgentMessage -> encoded AgentMessage' +agentRatchetDecrypt :: AgentMonad m => ByteString -> m ByteString +agentRatchetDecrypt = pure agentCbEncrypt :: AgentMonad m => SndQueue -> Maybe C.PublicKeyX25519 -> ByteString -> m ByteString agentCbEncrypt SndQueue {e2eDhSecret} e2ePubKey msg = do - emNonce <- liftIO C.randomCbNonce - emBody <- + cmNonce <- liftIO C.randomCbNonce + cmEncBody <- liftEither . first cryptoError $ - C.cbEncrypt e2eDhSecret emNonce msg SMP.e2eEncMessageLength + C.cbEncrypt e2eDhSecret cmNonce msg SMP.e2eEncMessageLength -- TODO per-queue client version - let emHeader = SMP.PubHeader (maxVersion SMP.smpClientVersion) e2ePubKey - pure $ smpEncode SMP.EncMessage {emHeader, emNonce, emBody} + let cmHeader = SMP.PubHeader (maxVersion SMP.smpClientVersion) e2ePubKey + pure $ smpEncode SMP.ClientMsgEnvelope {cmHeader, cmNonce, cmEncBody} +-- add encoding as AgentInvitation'? agentCbEncryptOnce :: AgentMonad m => C.PublicKeyX25519 -> ByteString -> m ByteString agentCbEncryptOnce dhRcvPubKey msg = do (dhSndPubKey, dhSndPrivKey) <- liftIO C.generateKeyPair' let e2eDhSecret = C.dh' dhRcvPubKey dhSndPrivKey - emNonce <- liftIO C.randomCbNonce - emBody <- + cmNonce <- liftIO C.randomCbNonce + cmEncBody <- liftEither . first cryptoError $ - C.cbEncrypt e2eDhSecret emNonce msg SMP.e2eEncMessageLength + C.cbEncrypt e2eDhSecret cmNonce msg SMP.e2eEncMessageLength -- TODO per-queue client version - let emHeader = SMP.PubHeader (maxVersion SMP.smpClientVersion) (Just dhSndPubKey) - pure $ smpEncode SMP.EncMessage {emHeader, emNonce, emBody} + let cmHeader = SMP.PubHeader (maxVersion SMP.smpClientVersion) (Just dhSndPubKey) + pure $ smpEncode SMP.ClientMsgEnvelope {cmHeader, cmNonce, cmEncBody} +-- | NaCl crypto-box decrypt - both for messages received from the server +-- and per-queue E2E encrypted messages from the sender that were inside. agentCbDecrypt :: AgentMonad m => C.DhSecretX25519 -> C.CbNonce -> ByteString -> m ByteString agentCbDecrypt dhSecret nonce msg = liftEither . first cryptoError $ diff --git a/src/Simplex/Messaging/Agent/Env/SQLite.hs b/src/Simplex/Messaging/Agent/Env/SQLite.hs index 0efe49749..247072fbf 100644 --- a/src/Simplex/Messaging/Agent/Env/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Env/SQLite.hs @@ -29,7 +29,6 @@ data AgentConfig = AgentConfig dbFile :: FilePath, dbPoolSize :: Int, smpCfg :: SMPClientConfig, - retryInterval :: RetryInterval, reconnectInterval :: RetryInterval, caCertificateFile :: FilePath, privateKeyFile :: FilePath, @@ -50,12 +49,6 @@ defaultAgentConfig = dbFile = "smp-agent.db", dbPoolSize = 4, smpCfg = smpDefaultConfig, - retryInterval = - RetryInterval - { initialInterval = 1_000_000, - increaseAfter = minute, - maxInterval = 10 * minute - }, reconnectInterval = RetryInterval { initialInterval = 1_000_000, diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index f1ad4c705..ad522063f 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -11,6 +11,7 @@ {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-} @@ -29,7 +30,11 @@ -- -- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/agent-protocol.md module Simplex.Messaging.Agent.Protocol - ( -- * SMP agent protocol types + ( -- * Protocol parameters + smpAgentVersion, + smpAgentVRange, + + -- * SMP agent protocol types ConnInfo, ACommand (..), AParty (..), @@ -37,23 +42,27 @@ module Simplex.Messaging.Agent.Protocol MsgHash, MsgMeta (..), SMPConfirmation (..), - AgentMessage (..), - AHeader (..), + AgentMsgEnvelope (..), + AgentMessage' (..), + APrivHeader (..), AMessage (..), + AMsgType (..), SMPServer (..), SrvLoc (..), SMPQueueUri (..), + SMPQueueInfo (..), ConnectionMode (..), SConnectionMode (..), AConnectionMode (..), cmInvitation, cmContact, ConnectionModeI (..), - ConnectionRequest (..), - AConnectionRequest (..), - ConnReqData (..), + ConnectionRequestUri (..), + AConnectionRequestUri (..), + ConnReqUriData (..), ConnReqScheme (..), - ConnectionEncryption (..), + E2ERatchetParams (..), + E2ERatchetParamsUri (..), simplexChat, AgentErrorType (..), CommandErrorType (..), @@ -71,11 +80,12 @@ module Simplex.Messaging.Agent.Protocol QueueStatus (..), ACorrId, AgentMsgId, + -- TODO remove + connEncStubUri, + connEncStub, - -- * Parse and serialize + -- * Encode/decode serializeCommand, - clientToAgentMsg, - serializeAgentMessage, serializeMsgIntegrity, connMode, connMode', @@ -88,6 +98,7 @@ module Simplex.Messaging.Agent.Protocol smpErrorTypeP, serializeQueueStatus, queueStatusT, + aMessageType, -- * TCP transport functions tPut, @@ -97,12 +108,11 @@ module Simplex.Messaging.Agent.Protocol ) where -import Control.Applicative ((<|>)) +import Control.Applicative (optional, (<|>)) import Control.Monad.IO.Class import Data.Attoparsec.ByteString.Char8 (Parser) import qualified Data.Attoparsec.ByteString.Char8 as A import Data.ByteString.Base64 -import qualified Data.ByteString.Base64.URL as U import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.Composition ((.:)) @@ -119,17 +129,15 @@ import Data.Type.Equality import Data.Typeable () import GHC.Generics (Generic) import Generic.Random (genericArbitraryU) -import Network.HTTP.Types (SimpleQuery, parseSimpleQuery, renderSimpleQuery) +import qualified Network.HTTP.Types as Q import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers import Simplex.Messaging.Protocol - ( ClientMessage (..), - ErrorType, + ( ErrorType, MsgBody, MsgId, - PrivHeader (..), SMPServer (..), SndPublicVerifyKey, SrvLoc (..), @@ -142,6 +150,12 @@ import Test.QuickCheck (Arbitrary (..)) import Text.Read import UnliftIO.Exception (Exception) +smpAgentVersion :: Version +smpAgentVersion = 1 + +smpAgentVRange :: VersionRange +smpAgentVRange = mkVersionRange 1 smpAgentVersion + -- | Raw (unparsed) SMP agent protocol transmission. type ARawTransmission = (ByteString, ByteString, ByteString) @@ -180,8 +194,8 @@ type ConnInfo = ByteString -- | Parameterized type for SMP agent protocol commands and responses from all participants. data ACommand (p :: AParty) where NEW :: AConnectionMode -> ACommand Client -- response INV - INV :: AConnectionRequest -> ACommand Agent - JOIN :: AConnectionRequest -> ConnInfo -> ACommand Client -- response OK + INV :: AConnectionRequestUri -> ACommand Agent + JOIN :: AConnectionRequestUri -> ConnInfo -> ACommand Client -- response OK CONF :: ConfirmationId -> ConnInfo -> ACommand Agent -- ConnInfo is from sender LET :: ConfirmationId -> ConnInfo -> ACommand Client -- ConnInfo is from client REQ :: InvitationId -> ConnInfo -> ACommand Agent -- ConnInfo is from sender @@ -272,29 +286,135 @@ data SMPConfirmation = SMPConfirmation } deriving (Show) --- SMP agent message formats -data AgentMessage - = AgentConfirmation C.APublicVerifyKey ConnInfo -- TODO add double ratchet E2E settings - | AgentInvitation (ConnectionRequest CMInvitation) ConnInfo - | AgentMessage AHeader AMessage +data AgentMsgEnvelope + = AgentConfirmation + { agentVersion :: Version, + e2eEncryption :: E2ERatchetParams, + encConnInfo :: ByteString + } + | AgentMsgEnvelope + { agentVersion :: Version, + encAgentMessage :: ByteString + } + | AgentInvitation' -- the connInfo in contactInvite is only encrypted with per-queue E2E, not with double ratchet, + { agentVersion :: !Version, + connReq :: !(ConnectionRequestUri 'CMInvitation), + connInfo :: !ByteString -- this message is only encrypted with per-queue E2E, not with double ratchet, + } + deriving (Show) -data AHeader = AHeader +instance Encoding AgentMsgEnvelope where + smpEncode = \case + AgentConfirmation {agentVersion, e2eEncryption, encConnInfo} -> + smpEncode (agentVersion, 'C', e2eEncryption, Tail encConnInfo) + AgentMsgEnvelope {agentVersion, encAgentMessage} -> + smpEncode (agentVersion, 'M', Tail encAgentMessage) + AgentInvitation' {agentVersion, connReq, connInfo} -> + smpEncode (agentVersion, 'I', Large $ strEncode connReq, Tail connInfo) + smpP = do + agentVersion <- smpP + smpP >>= \case + 'C' -> do + (e2eEncryption, Tail encConnInfo) <- smpP + pure AgentConfirmation {agentVersion, e2eEncryption, encConnInfo} + 'M' -> do + Tail encAgentMessage <- smpP + pure AgentMsgEnvelope {agentVersion, encAgentMessage} + 'I' -> do + connReq <- strDecode . unLarge <$?> smpP + Tail connInfo <- smpP + pure AgentInvitation' {agentVersion, connReq, connInfo} + _ -> fail "bad AgentMsgEnvelope" + +data E2ERatchetParams + = E2ERatchetParams Version C.PublicKeyX448 C.PublicKeyX448 + deriving (Eq, Show) + +instance Encoding E2ERatchetParams where + smpEncode (E2ERatchetParams v k1 k2) = smpEncode (v, k1, k2) + smpP = E2ERatchetParams <$> smpP <*> smpP <*> smpP + +instance VersionI E2ERatchetParams where + type VersionRangeT E2ERatchetParams = E2ERatchetParamsUri + version (E2ERatchetParams v _ _) = v + toVersionRangeT (E2ERatchetParams _ k1 k2) vr = E2ERatchetParamsUri vr k1 k2 + +instance VersionRangeI E2ERatchetParamsUri where + type VersionT E2ERatchetParamsUri = E2ERatchetParams + versionRange (E2ERatchetParamsUri vr _ _) = vr + toVersionT (E2ERatchetParamsUri _ k1 k2) v = E2ERatchetParams v k1 k2 + +data E2ERatchetParamsUri + = E2ERatchetParamsUri VersionRange C.PublicKeyX448 C.PublicKeyX448 + deriving (Eq, Show) + +connEncStubUri :: E2ERatchetParamsUri +connEncStubUri = E2ERatchetParamsUri smpAgentVRange stubDhPubKey stubDhPubKey + +connEncStub :: E2ERatchetParams +connEncStub = E2ERatchetParams smpAgentVersion stubDhPubKey stubDhPubKey + +stubDhPubKey :: C.PublicKeyX448 +stubDhPubKey = "MEIwBQYDK2VvAzkAmKuSYeQ/m0SixPDS8Wq8VBaTS1cW+Lp0n0h4Diu+kUpR+qXx4SDJ32YGEFoGFGSbGPry5Ychr6U=" + +instance StrEncoding E2ERatchetParamsUri where + strEncode (E2ERatchetParamsUri vs key1 key2) = + strEncode $ + QSP QNoEscaping [("v", strEncode vs), ("x3dh", strEncode [key1, key2])] + strP = do + query <- strP + vs <- queryParam "v" query + keys <- queryParam "x3dh" query + case keys of + [key1, key2] -> pure $ E2ERatchetParamsUri vs key1 key2 + _ -> fail "bad e2e params" + +-- SMP agent message formats (after double ratchet decryption, +-- or in case of AgentInvitation - in plain text body) +data AgentMessage' = AgentConnInfo ConnInfo | AgentMessage' APrivHeader AMessage + +instance Encoding AgentMessage' where + smpEncode = \case + AgentConnInfo cInfo -> smpEncode ('I', cInfo) + AgentMessage' hdr aMsg -> smpEncode ('M', hdr, aMsg) + smpP = + smpP >>= \case + 'I' -> AgentConnInfo <$> smpP + 'M' -> AgentMessage' <$> smpP <*> smpP + _ -> fail "bad AgentMessage" + +data APrivHeader = APrivHeader { -- | sequential ID assigned by the sending agent sndMsgId :: AgentMsgId, -- | digest of the previous message prevMsgHash :: MsgHash } -instance StrEncoding AHeader where - strEncode AHeader {sndMsgId, prevMsgHash} = - bshow sndMsgId <> " " <> strEncode prevMsgHash <> "\n" - strP = AHeader <$> A.decimal <* A.space <*> (strP <|> pure "") <* A.endOfLine +instance Encoding APrivHeader where + smpEncode APrivHeader {sndMsgId, prevMsgHash} = + smpEncode (sndMsgId, prevMsgHash) + smpP = APrivHeader <$> smpP <*> smpP -emptyAHeader :: ByteString -emptyAHeader = "\n" +data AMsgType = HELLO_ | REPLY_ | A_MSG_ + deriving (Eq) -emptyAHeaderP :: Parser () -emptyAHeaderP = A.endOfLine $> () +instance Encoding AMsgType where + smpEncode = \case + HELLO_ -> "H" + REPLY_ -> "R" + A_MSG_ -> "M" + smpP = + smpP >>= \case + 'H' -> pure HELLO_ + 'R' -> pure REPLY_ + 'M' -> pure A_MSG_ + _ -> fail "bad AMsgType" + +aMessageType :: AMessage -> AMsgType +aMessageType = \case + HELLO -> HELLO_ + REPLY _ -> REPLY_ + A_MSG _ -> A_MSG_ -- | Messages sent between SMP agents once SMP queue is secured. -- @@ -302,93 +422,83 @@ emptyAHeaderP = A.endOfLine $> () data AMessage = -- | the first message in the queue to validate it is secured HELLO - | -- | reply queue information - REPLY (ConnectionRequest CMInvitation) + | -- | reply queues information + REPLY (L.NonEmpty SMPQueueInfo) | -- | agent envelope for the client message A_MSG MsgBody deriving (Show) -serializeAgentMessage :: AgentMessage -> ByteString -serializeAgentMessage = smpEncode . agentToClientMsg +instance Encoding AMessage where + smpEncode = \case + HELLO -> smpEncode HELLO_ + REPLY smpQueues -> smpEncode (REPLY_, smpQueues) + A_MSG body -> smpEncode (A_MSG_, Tail body) + smpP = + smpP + >>= \case + HELLO_ -> pure HELLO + REPLY_ -> REPLY <$> smpP + A_MSG_ -> A_MSG . unTail <$> smpP -agentToClientMsg :: AgentMessage -> ClientMessage -agentToClientMsg = \case - AgentConfirmation senderKey cInfo -> - ClientMessage (PHConfirmation senderKey) $ emptyAHeader <> cInfo - AgentInvitation cReq cInfo -> - ClientMessage PHEmpty $ emptyAHeader <> strEncode cReq <> "\n" <> cInfo - AgentMessage header aMsg -> - ClientMessage PHEmpty $ strEncode header <> strEncode aMsg +data QueryStringParams = QSP QSPEscaping Q.SimpleQuery + deriving (Show) -clientToAgentMsg :: ClientMessage -> Either AgentErrorType AgentMessage -clientToAgentMsg (ClientMessage header body) = parse parser (AGENT A_MESSAGE) body - where - parser = case header of - PHConfirmation senderKey -> AgentConfirmation senderKey <$> (emptyAHeaderP *> A.takeByteString) - PHEmpty -> invitationP <|> messageP - invitationP = AgentInvitation <$> (emptyAHeaderP *> strP <* A.endOfLine) <*> A.takeByteString - messageP = AgentMessage <$> strP <*> strP - -instance StrEncoding AMessage where - strP = - "HELLO" $> HELLO - <|> "REPLY " *> (REPLY <$> strP) - <|> "MSG " *> (A_MSG <$> A.takeByteString) - strEncode = \case - HELLO -> "HELLO" - REPLY cReq -> "REPLY " <> strEncode cReq - A_MSG body -> "MSG " <> body - -instance StrEncoding SMPQueueUri where - strEncode SMPQueueUri {smpServer = srv, senderId = qId, smpVersionRange = vr, dhPublicKey = k} = - strEncode srv <> "/" <> U.encode qId <> "#" <> strEncode k - strP = do - smpServer <- strP <* A.char '/' - senderId <- strP <* A.char '#' - let smpVersionRange = SMP.smpClientVersion - dhPublicKey <- strP - pure SMPQueueUri {smpServer, senderId, smpVersionRange, dhPublicKey} - -newtype QueryStringParams = QSP SimpleQuery +data QSPEscaping = QEscape | QNoEscaping + deriving (Show) instance StrEncoding QueryStringParams where - strEncode (QSP q) = renderSimpleQuery True q - strP = QSP . parseSimpleQuery <$> A.takeTill (\c -> c == ' ' || c == '\n') + strEncode (QSP esc q) = case esc of + QEscape -> Q.renderSimpleQuery False q + QNoEscaping -> + Q.renderQueryPartialEscape False $ + map (\(n, v) -> (n, [Q.QN v])) q + strP = QSP QEscape . Q.parseSimpleQuery <$> A.takeTill (\c -> c == ' ' || c == '\n') queryParam :: StrEncoding a => ByteString -> QueryStringParams -> Parser a -queryParam name (QSP q) = +queryParam name (QSP _ q) = case find ((== name) . fst) q of Just (_, p) -> either fail pure $ parseAll strP p _ -> fail $ "no qs param " <> B.unpack name -instance forall m. ConnectionModeI m => StrEncoding (ConnectionRequest m) where +instance forall m. ConnectionModeI m => StrEncoding (ConnectionRequestUri m) where strEncode = \case - CRInvitation crData -> serialize "invitation" crData - CRContact crData -> serialize "contact" crData + CRInvitationUri crData e2eParams -> crEncode "invitation" crData (Just e2eParams) + CRContactUri crData -> crEncode "contact" crData Nothing where - serialize crMode ConnReqData {crScheme, crSmpQueues, crEncryption = _} = - strEncode crScheme <> "/" <> crMode <> "#/" <> queryStr + crEncode :: ByteString -> ConnReqUriData -> Maybe E2ERatchetParamsUri -> ByteString + crEncode crMode ConnReqUriData {crScheme, crAgentVRange, crSmpQueues} e2eParams = + strEncode crScheme <> "/" <> crMode <> "#/?" <> queryStr where - queryStr = strEncode $ QSP [("smp", strEncode crSmpQueues), ("e2e", "")] + queryStr = + strEncode . QSP QEscape $ + [("v", strEncode crAgentVRange), ("smp", strEncode crSmpQueues)] + <> maybe [] (\e2e -> [("e2e", strEncode e2e)]) e2eParams strP = do - ACR m cr <- strP + ACRU m cr <- strP case testEquality m $ sConnectionMode @m of Just Refl -> pure cr _ -> fail "bad connection request mode" -instance StrEncoding AConnectionRequest where - strEncode (ACR _ cr) = strEncode cr +instance StrEncoding AConnectionRequestUri where + strEncode (ACRU _ cr) = strEncode cr strP = do crScheme <- strP - mkConnReq <- "/" *> mkConnReqP <* "#/?" + crMode <- A.char '/' *> crModeP <* optional (A.char '/') <* "#/?" query <- strP + crAgentVRange <- queryParam "v" query crSmpQueues <- queryParam "smp" query - let crEncryption = ConnectionEncryption - pure $ mkConnReq ConnReqData {crScheme, crSmpQueues, crEncryption} + let crData = ConnReqUriData {crScheme, crAgentVRange, crSmpQueues} + case crMode of + CMInvitation -> do + crE2eParams <- queryParam "e2e" query + pure . ACRU SCMInvitation $ CRInvitationUri crData crE2eParams + CMContact -> pure . ACRU SCMContact $ CRContactUri crData where - mkConnReqP = - "invitation" $> ACR SCMInvitation . CRInvitation - <|> "contact" $> ACR SCMContact . CRContact + crModeP = "invitation" $> CMInvitation <|> "contact" $> CMContact + +-- debug :: Show a => String -> a -> a +-- debug name value = unsafePerformIO (putStrLn $ name <> ": " <> show value) `seq` value +-- {-# INLINE debug #-} instance StrEncoding ConnectionMode where strEncode = \case @@ -413,38 +523,90 @@ type ConfirmationId = ByteString type InvitationId = ByteString +data SMPQueueInfo = SMPQueueInfo + { clientVersion :: Version, + smpServer :: SMPServer, + senderId :: SMP.SenderId, + dhPublicKey :: C.PublicKeyX25519 + } + deriving (Eq, Show) + +instance Encoding SMPQueueInfo where + smpEncode SMPQueueInfo {clientVersion, smpServer, senderId, dhPublicKey} = + smpEncode (clientVersion, smpServer, senderId, dhPublicKey) + smpP = do + (clientVersion, smpServer, senderId, dhPublicKey) <- smpP + pure SMPQueueInfo {clientVersion, smpServer, senderId, dhPublicKey} + +-- This instance seems contrived and there was a temptation to split a common part of both types. +-- But this is created to allow backward and forward compatibility where SMPQueueUri +-- could have more fields to convert to different versions of SMPQueueInfo in a different way, +-- and this instance would become non-trivial. +instance VersionI SMPQueueInfo where + type VersionRangeT SMPQueueInfo = SMPQueueUri + version = clientVersion + toVersionRangeT SMPQueueInfo {smpServer, senderId, dhPublicKey} vr = + SMPQueueUri {clientVersionRange = vr, smpServer, senderId, dhPublicKey} + +instance VersionRangeI SMPQueueUri where + type VersionT SMPQueueUri = SMPQueueInfo + versionRange = clientVersionRange + toVersionT SMPQueueUri {smpServer, senderId, dhPublicKey} v = + SMPQueueInfo {clientVersion = v, smpServer, senderId, dhPublicKey} + -- | SMP queue information sent out-of-band. -- -- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#out-of-band-messages data SMPQueueUri = SMPQueueUri { smpServer :: SMPServer, senderId :: SMP.SenderId, - smpVersionRange :: VersionRange, + clientVersionRange :: VersionRange, dhPublicKey :: C.PublicKeyX25519 } deriving (Eq, Show) -data ConnectionRequest (m :: ConnectionMode) where - CRInvitation :: ConnReqData -> ConnectionRequest CMInvitation - CRContact :: ConnReqData -> ConnectionRequest CMContact +-- TODO change SMP queue URI format to include version range and allow unknown parameters +instance StrEncoding SMPQueueUri where + -- v1 uses short SMP queue URI format + strEncode SMPQueueUri {smpServer = srv, senderId = qId, clientVersionRange = _vr, dhPublicKey = k} = + strEncode srv <> "/" <> strEncode qId <> "#" <> strEncode k + strP = do + smpServer <- strP <* A.char '/' + senderId <- strP <* optional (A.char '/') <* A.char '#' + (vr, dhPublicKey) <- unversioned <|> versioned + pure SMPQueueUri {smpServer, senderId, clientVersionRange = vr, dhPublicKey} + where + unversioned = (SMP.smpClientVersion,) <$> strP <* A.endOfInput + versioned = do + dhKey_ <- optional strP + query <- optional (A.char '/') *> A.char '?' *> strP + vr <- queryParam "v" query + dhKey <- maybe (queryParam "dh" query) pure dhKey_ + pure (vr, dhKey) -deriving instance Eq (ConnectionRequest m) +data ConnectionRequestUri (m :: ConnectionMode) where + CRInvitationUri :: ConnReqUriData -> E2ERatchetParamsUri -> ConnectionRequestUri CMInvitation + -- contact connection request does NOT contain E2E encryption parameters - + -- they are passed in AgentInvitation message + CRContactUri :: ConnReqUriData -> ConnectionRequestUri CMContact -deriving instance Show (ConnectionRequest m) +deriving instance Eq (ConnectionRequestUri m) -data AConnectionRequest = forall m. ConnectionModeI m => ACR (SConnectionMode m) (ConnectionRequest m) +deriving instance Show (ConnectionRequestUri m) -instance Eq AConnectionRequest where - ACR m cr == ACR m' cr' = case testEquality m m' of +data AConnectionRequestUri = forall m. ConnectionModeI m => ACRU (SConnectionMode m) (ConnectionRequestUri m) + +instance Eq AConnectionRequestUri where + ACRU m cr == ACRU m' cr' = case testEquality m m' of Just Refl -> cr == cr' _ -> False -deriving instance Show AConnectionRequest +deriving instance Show AConnectionRequestUri -data ConnReqData = ConnReqData +data ConnReqUriData = ConnReqUriData { crScheme :: ConnReqScheme, - crSmpQueues :: L.NonEmpty SMPQueueUri, - crEncryption :: ConnectionEncryption + crAgentVRange :: VersionRange, + crSmpQueues :: L.NonEmpty SMPQueueUri } deriving (Eq, Show) @@ -459,10 +621,6 @@ instance StrEncoding ConnReqScheme where "simplex:" $> CRSSimplex <|> "https://" *> (CRSAppServer <$> strP) --- TODO this is a stub for double ratchet E2E encryption parameters (2 public DH keys) -data ConnectionEncryption = ConnectionEncryption - deriving (Eq, Show) - simplexChat :: ConnReqScheme simplexChat = CRSAppServer $ SrvLoc "simplex.chat" Nothing @@ -562,11 +720,14 @@ data BrokerErrorType deriving (Eq, Generic, Read, Show, Exception) -- | Errors of another SMP agent. +-- TODO encode/decode without A prefix data SMPAgentError = -- | client or agent message that failed to parse A_MESSAGE | -- | prohibited SMP/agent message A_PROHIBITED + | -- | incompatible version of SMP client, agent or encryption protocols + A_VERSION | -- | cannot decrypt message A_ENCRYPTION deriving (Eq, Generic, Read, Show, Exception) @@ -630,7 +791,7 @@ commandP = broker <- " B=" *> partyMeta base64P sndMsgId <- " S=" *> A.decimal pure MsgMeta {integrity, recipient, broker, sndMsgId} - partyMeta idParser = (,) <$> idParser <* "," <*> tsISO8601P + partyMeta idParser = (,) <$> idParser <* A.char ',' <*> tsISO8601P agentError = ACmd SAgent . ERR <$> agentErrorTypeP -- | Message integrity validation result parser. diff --git a/src/Simplex/Messaging/Agent/Store.hs b/src/Simplex/Messaging/Agent/Store.hs index ee99611d6..d46fecff8 100644 --- a/src/Simplex/Messaging/Agent/Store.hs +++ b/src/Simplex/Messaging/Agent/Store.hs @@ -64,8 +64,7 @@ class Monad m => MonadAgentStore s m where createRcvMsg :: s -> ConnId -> RcvMsgData -> m () updateSndIds :: s -> ConnId -> m (InternalId, InternalSndId, PrevSndMsgHash) createSndMsg :: s -> ConnId -> SndMsgData -> m () - updateSndMsgStatus :: s -> ConnId -> InternalId -> SndMsgStatus -> m () - getPendingMsgData :: s -> ConnId -> InternalId -> m (SndQueue, MsgBody) + getPendingMsgData :: s -> ConnId -> InternalId -> m (SndQueue, Maybe RcvQueue, (AMsgType, MsgBody)) getPendingMsgs :: s -> ConnId -> m [InternalId] getMsg :: s -> ConnId -> InternalId -> m Msg checkRcvMsg :: s -> ConnId -> InternalId -> m () @@ -187,14 +186,14 @@ data AcceptedConfirmation = AcceptedConfirmation data NewInvitation = NewInvitation { contactConnId :: ConnId, - connReq :: ConnectionRequest 'CMInvitation, + connReq :: ConnectionRequestUri 'CMInvitation, recipientConnInfo :: ConnInfo } data Invitation = Invitation { invitationId :: InvitationId, contactConnId :: ConnId, - connReq :: ConnectionRequest 'CMInvitation, + connReq :: ConnectionRequestUri 'CMInvitation, recipientConnInfo :: ConnInfo, ownConnInfo :: Maybe ConnInfo, accepted :: Bool @@ -217,6 +216,7 @@ type PrevSndMsgHash = MsgHash data RcvMsgData = RcvMsgData { msgMeta :: MsgMeta, + msgType :: AMsgType, msgBody :: MsgBody, internalRcvId :: InternalRcvId, internalHash :: MsgHash, @@ -227,6 +227,7 @@ data SndMsgData = SndMsgData { internalId :: InternalId, internalSndId :: InternalSndId, internalTs :: InternalTs, + msgType :: AMsgType, msgBody :: MsgBody, internalHash :: MsgHash, prevMsgHash :: MsgHash diff --git a/src/Simplex/Messaging/Agent/Store/SQLite.hs b/src/Simplex/Messaging/Agent/Store/SQLite.hs index 537e1efe7..037a24956 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite.hs @@ -52,6 +52,7 @@ import Simplex.Messaging.Agent.Store import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration) import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations import qualified Simplex.Messaging.Crypto as C +import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (blobFieldParser) import Simplex.Messaging.Protocol (MsgBody) @@ -438,41 +439,27 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto insertSndMsgDetails_ db connId sndMsgData updateHashSnd_ db connId sndMsgData - updateSndMsgStatus :: SQLiteStore -> ConnId -> InternalId -> SndMsgStatus -> m () - updateSndMsgStatus st connId msgId msgStatus = - liftIO . withTransaction st $ \db -> - DB.executeNamed - db - [sql| - UPDATE snd_messages - SET snd_status = :snd_status - WHERE conn_alias = :conn_alias AND internal_id = :internal_id - |] - [ ":conn_alias" := connId, - ":internal_id" := msgId, - ":snd_status" := msgStatus - ] - - getPendingMsgData :: SQLiteStore -> ConnId -> InternalId -> m (SndQueue, MsgBody) + getPendingMsgData :: SQLiteStore -> ConnId -> InternalId -> m (SndQueue, Maybe RcvQueue, (AMsgType, MsgBody)) getPendingMsgData st connId msgId = liftIOEither . withTransaction st $ \db -> runExceptT $ do sq <- ExceptT $ sndQueue <$> getSndQueueByConnAlias_ db connId - msgBody <- + rq_ <- liftIO $ getRcvQueueByConnAlias_ db connId + msgData <- ExceptT $ sndMsgData <$> DB.query db [sql| - SELECT m.msg_body + SELECT m.msg_type, m.msg_body FROM messages m JOIN snd_messages s ON s.conn_alias = m.conn_alias AND s.internal_id = m.internal_id WHERE m.conn_alias = ? AND m.internal_id = ? |] (connId, msgId) - pure (sq, msgBody) + pure (sq, rq_, msgData) where - sndMsgData :: [Only MsgBody] -> Either StoreError MsgBody - sndMsgData [Only msgBody] = Right msgBody + sndMsgData :: [(AMsgType, MsgBody)] -> Either StoreError (AMsgType, MsgBody) + sndMsgData [msgData] = Right msgData sndMsgData _ = Left SEMsgNotFound sndQueue :: Maybe SndQueue -> Either StoreError SndQueue sndQueue = maybe (Left SEConnNotFound) Right @@ -529,6 +516,10 @@ instance ToField RcvMsgStatus where toField = toField . serializeRcvMsgStatus instance FromField RcvMsgStatus where fromField = fromTextField_ rcvMsgStatusT +instance ToField AMsgType where toField = toField . smpEncode + +instance FromField AMsgType where fromField = blobFieldParser smpP + instance ToField SndMsgStatus where toField = toField . serializeSndMsgStatus instance FromField SndMsgStatus where fromField = fromTextField_ sndMsgStatusT @@ -541,13 +532,13 @@ instance ToField SMPQueueUri where toField = toField . strEncode instance FromField SMPQueueUri where fromField = blobFieldParser strP -instance ToField AConnectionRequest where toField = toField . strEncode +instance ToField AConnectionRequestUri where toField = toField . strEncode -instance FromField AConnectionRequest where fromField = blobFieldParser strP +instance FromField AConnectionRequestUri where fromField = blobFieldParser strP -instance ConnectionModeI c => ToField (ConnectionRequest c) where toField = toField . strEncode +instance ConnectionModeI c => ToField (ConnectionRequestUri c) where toField = toField . strEncode -instance (E.Typeable c, ConnectionModeI c) => FromField (ConnectionRequest c) where fromField = blobFieldParser strP +instance (E.Typeable c, ConnectionModeI c) => FromField (ConnectionRequestUri c) where fromField = blobFieldParser strP instance ToField ConnectionMode where toField = toField . decodeLatin1 . strEncode @@ -747,20 +738,21 @@ updateLastIdsRcv_ dbConn connId newInternalId newInternalRcvId = -- * createRcvMsg helpers insertRcvMsgBase_ :: DB.Connection -> ConnId -> RcvMsgData -> IO () -insertRcvMsgBase_ dbConn connId RcvMsgData {msgMeta, msgBody, internalRcvId} = do +insertRcvMsgBase_ dbConn connId RcvMsgData {msgMeta, msgType, msgBody, internalRcvId} = do let MsgMeta {recipient = (internalId, internalTs)} = msgMeta DB.executeNamed dbConn [sql| INSERT INTO messages - ( conn_alias, internal_id, internal_ts, internal_rcv_id, internal_snd_id, msg_body) + ( conn_alias, internal_id, internal_ts, internal_rcv_id, internal_snd_id, msg_type, msg_body) VALUES - (:conn_alias,:internal_id,:internal_ts,:internal_rcv_id, NULL,:msg_body); + (:conn_alias,:internal_id,:internal_ts,:internal_rcv_id, NULL,:msg_type, :msg_body); |] [ ":conn_alias" := connId, ":internal_id" := internalId, ":internal_ts" := internalTs, ":internal_rcv_id" := internalRcvId, + ":msg_type" := msgType, ":msg_body" := msgBody ] @@ -847,14 +839,15 @@ insertSndMsgBase_ dbConn connId SndMsgData {..} = do dbConn [sql| INSERT INTO messages - ( conn_alias, internal_id, internal_ts, internal_rcv_id, internal_snd_id, msg_body) + ( conn_alias, internal_id, internal_ts, internal_rcv_id, internal_snd_id, msg_type, msg_body) VALUES - (:conn_alias,:internal_id,:internal_ts, NULL,:internal_snd_id,:msg_body); + (:conn_alias,:internal_id,:internal_ts, NULL,:internal_snd_id,:msg_type, :msg_body); |] [ ":conn_alias" := connId, ":internal_id" := internalId, ":internal_ts" := internalTs, ":internal_snd_id" := internalSndId, + ":msg_type" := msgType, ":msg_body" := msgBody ] diff --git a/src/Simplex/Messaging/Crypto.hs b/src/Simplex/Messaging/Crypto.hs index 17ba5776e..bf94f8931 100644 --- a/src/Simplex/Messaging/Crypto.hs +++ b/src/Simplex/Messaging/Crypto.hs @@ -40,6 +40,8 @@ module Simplex.Messaging.Crypto PublicKey (..), PrivateKeyX25519, PublicKeyX25519, + PrivateKeyX448, + PublicKeyX448, APrivateKey (..), APublicKey (..), APrivateSignKey (..), @@ -63,10 +65,6 @@ module Simplex.Messaging.Crypto encodePubKey, encodePrivKey, - -- * E2E hybrid encryption scheme - E2EEncryptionVersion, - currentE2EVersion, - -- * sign/verify Signature (..), ASignature (..), @@ -86,16 +84,14 @@ module Simplex.Messaging.Crypto -- * AES256 AEAD-GCM scheme Key (..), IV (..), + AuthTag (..), encryptAES, decryptAES, encryptAEAD, decryptAEAD, authTagSize, - authTagToBS, - bsToAuthTag, randomAesKey, randomIV, - ivP, ivSize, -- * NaCl crypto_box @@ -134,7 +130,6 @@ import Crypto.Random (getRandomBytes) import Data.ASN1.BinaryEncoding import Data.ASN1.Encoding import Data.ASN1.Types -import Data.Attoparsec.ByteString.Char8 (Parser) import qualified Data.Attoparsec.ByteString.Char8 as A import Data.Bifunctor (bimap, first) import qualified Data.ByteArray as BA @@ -148,7 +143,6 @@ import Data.Kind (Constraint, Type) import Data.String import Data.Type.Equality import Data.Typeable (Typeable) -import Data.Word (Word16) import Data.X509 import Database.SQLite.Simple.FromField (FromField (..)) import Database.SQLite.Simple.ToField (ToField (..)) @@ -158,11 +152,6 @@ import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (blobFieldDecoder, parseAll, parseString) -type E2EEncryptionVersion = Word16 - -currentE2EVersion :: E2EEncryptionVersion -currentE2EVersion = 1 - -- | Cryptographic algorithms. data Algorithm = Ed25519 | Ed448 | X25519 | X448 @@ -236,6 +225,8 @@ deriving instance Show APublicKey type PublicKeyX25519 = PublicKey X25519 +type PublicKeyX448 = PublicKey X448 + -- | GADT for private keys. data PrivateKey (a :: Algorithm) where PrivateKeyEd25519 :: Ed25519.SecretKey -> Ed25519.PublicKey -> PrivateKey Ed25519 @@ -261,6 +252,8 @@ deriving instance Show APrivateKey type PrivateKeyX25519 = PrivateKey X25519 +type PrivateKeyX448 = PrivateKey X448 + type family SignatureAlgorithm (a :: Algorithm) :: Constraint where SignatureAlgorithm Ed25519 = () SignatureAlgorithm Ed448 = () @@ -519,7 +512,7 @@ instance AlgorithmI a => ToField (PrivateKey a) where toField = toField . encode instance AlgorithmI a => ToField (PublicKey a) where toField = toField . encodePubKey -instance AlgorithmI a => ToField (DhSecret a) where toField = toField . dhSecretBytes' +instance ToField (DhSecret a) where toField = toField . dhSecretBytes' instance FromField APrivateSignKey where fromField = blobFieldDecoder decodePrivKey @@ -660,6 +653,16 @@ newtype Key = Key {unKey :: ByteString} -- | IV bytes newtype. newtype IV = IV {unIV :: ByteString} +instance Encoding IV where + smpEncode = unIV + smpP = IV <$> A.take (ivSize @AES256) + +newtype AuthTag = AuthTag {unAuthTag :: AES.AuthTag} + +instance Encoding AuthTag where + smpEncode = B.pack . map w2c . BA.unpack . AES.unAuthTag . unAuthTag + smpP = AuthTag . AES.AuthTag . BA.pack . map c2w . B.unpack <$> A.take authTagSize + -- | Certificate fingerpint newtype. -- -- Previously was used for server's public key hash in ad-hoc transport scheme, kept as is for compatibility. @@ -684,36 +687,32 @@ instance FromField KeyHash where fromField = blobFieldDecoder $ parseAll strP sha256Hash :: ByteString -> ByteString sha256Hash = BA.convert . (hash :: ByteString -> Digest SHA256) --- | IV bytes parser. -ivP :: Parser IV -ivP = IV <$> A.take (ivSize @AES256) - -- | AEAD-GCM encryption with empty associated data. -- -- Used as part of hybrid E2E encryption scheme and for SMP transport blocks encryption. -encryptAES :: Key -> IV -> Int -> ByteString -> ExceptT CryptoError IO (AES.AuthTag, ByteString) +encryptAES :: Key -> IV -> Int -> ByteString -> ExceptT CryptoError IO (AuthTag, ByteString) encryptAES key iv paddedLen = encryptAEAD key iv paddedLen "" -- | AEAD-GCM encryption. -- -- Used as part of hybrid E2E encryption scheme and for SMP transport blocks encryption. -encryptAEAD :: Key -> IV -> Int -> ByteString -> ByteString -> ExceptT CryptoError IO (AES.AuthTag, ByteString) +encryptAEAD :: Key -> IV -> Int -> ByteString -> ByteString -> ExceptT CryptoError IO (AuthTag, ByteString) encryptAEAD aesKey ivBytes paddedLen ad msg = do aead <- initAEAD @AES256 aesKey ivBytes msg' <- liftEither $ pad msg paddedLen - return $ AES.aeadSimpleEncrypt aead ad msg' authTagSize + pure . first AuthTag $ AES.aeadSimpleEncrypt aead ad msg' authTagSize -- | AEAD-GCM decryption with empty associated data. -- -- Used as part of hybrid E2E encryption scheme and for SMP transport blocks decryption. -decryptAES :: Key -> IV -> ByteString -> AES.AuthTag -> ExceptT CryptoError IO ByteString +decryptAES :: Key -> IV -> ByteString -> AuthTag -> ExceptT CryptoError IO ByteString decryptAES key iv = decryptAEAD key iv "" -- | AEAD-GCM decryption. -- -- Used as part of hybrid E2E encryption scheme and for SMP transport blocks decryption. -decryptAEAD :: Key -> IV -> ByteString -> ByteString -> AES.AuthTag -> ExceptT CryptoError IO ByteString -decryptAEAD aesKey ivBytes ad msg authTag = do +decryptAEAD :: Key -> IV -> ByteString -> ByteString -> AuthTag -> ExceptT CryptoError IO ByteString +decryptAEAD aesKey ivBytes ad msg (AuthTag authTag) = do aead <- initAEAD @AES256 aesKey ivBytes liftEither . unPad =<< maybeError AESDecryptError (AES.aeadSimpleDecrypt aead ad msg authTag) @@ -757,14 +756,6 @@ makeIV bs = maybeError CryptoIVError $ AES.makeIV bs maybeError :: CryptoError -> Maybe a -> ExceptT CryptoError IO a maybeError e = maybe (throwE e) return --- | Convert AEAD 'AuthTag' to ByteString. -authTagToBS :: AES.AuthTag -> ByteString -authTagToBS = B.pack . map w2c . BA.unpack . AES.unAuthTag - --- | Convert ByteString to AEAD 'AuthTag'. -bsToAuthTag :: ByteString -> AES.AuthTag -bsToAuthTag = AES.AuthTag . BA.pack . map c2w . B.unpack - cryptoFailable :: CE.CryptoFailable a -> ExceptT CryptoError IO a cryptoFailable = liftEither . first AESCipherError . CE.eitherCryptoError diff --git a/src/Simplex/Messaging/Crypto/Ratchet.hs b/src/Simplex/Messaging/Crypto/Ratchet.hs index e88f4b102..26c1ebd25 100644 --- a/src/Simplex/Messaging/Crypto/Ratchet.hs +++ b/src/Simplex/Messaging/Crypto/Ratchet.hs @@ -12,11 +12,8 @@ module Simplex.Messaging.Crypto.Ratchet where import Control.Monad.Except import Control.Monad.Trans.Except import Crypto.Cipher.AES (AES256) -import qualified Crypto.Cipher.Types as AES import Crypto.Hash (SHA512) import qualified Crypto.KDF.HKDF as H -import Data.Attoparsec.ByteString.Char8 (Parser) -import qualified Data.Attoparsec.ByteString.Char8 as A import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.Map.Strict (Map) @@ -27,10 +24,17 @@ import Simplex.Messaging.Crypto import Simplex.Messaging.Encoding import Simplex.Messaging.Parsers (parseE, parseE') import Simplex.Messaging.Util (tryE) +import Simplex.Messaging.Version + +e2eEncryptVersion :: Version +e2eEncryptVersion = 1 + +e2eEncryptVRange :: VersionRange +e2eEncryptVRange = mkVersionRange 1 e2eEncryptVersion data Ratchet a = Ratchet - { -- current ratchet version - rcVersion :: E2EEncryptionVersion, + { -- ratchet version range sent in messages (current .. max supported ratchet version) + rcVersion :: VersionRange, -- associated data - must be the same in both parties ratchets rcAD :: ByteString, rcDHRs :: KeyPair a, @@ -83,7 +87,7 @@ initSndRatchet' rcDHRr sPKey salt rcAD = do (rcRK, rcCKs, rcNHKs) = rootKdf sk rcDHRr pk pure Ratchet - { rcVersion = currentE2EVersion, + { rcVersion = e2eEncryptVRange, rcAD, rcDHRs, rcRK, @@ -107,7 +111,7 @@ initRcvRatchet' sKey rcDHRs@(_, pk) salt rcAD = do let (sk, rcNHKr, rcNHKs) = initKdf salt sKey pk pure Ratchet - { rcVersion = currentE2EVersion, + { rcVersion = e2eEncryptVRange, rcAD, rcDHRs, rcRK = sk, @@ -122,10 +126,8 @@ initRcvRatchet' sKey rcDHRs@(_, pk) salt rcAD = do } data MsgHeader a = MsgHeader - { -- | current E2E version - msgVersion :: E2EEncryptionVersion, - -- | latest E2E version supported by sending clients (to simplify version upgrade) - msgLatestVersion :: E2EEncryptionVersion, + { -- | max supported ratchet version + msgMaxVersion :: Version, msgDHRs :: PublicKey a, msgPN :: Word32, msgNs :: Word32 @@ -137,70 +139,69 @@ data AMsgHeader (AlgorithmI a, DhAlgorithm a) => AMsgHeader (SAlgorithm a) (MsgHeader a) +-- to allow extension without increasing the size, the actual header length is: +-- 81 = 2 (original size) + 2 + 1 + 68 (Ed448) + 4 + 4 paddedHeaderLen :: Int -paddedHeaderLen = 128 +paddedHeaderLen = 96 +-- 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 = paddedHeaderLen + authTagSize + ivSize @AES256 +fullHeaderLen = 2 + 1 + paddedHeaderLen + authTagSize + ivSize @AES256 instance AlgorithmI a => Encoding (MsgHeader a) where - smpEncode MsgHeader {msgVersion, msgLatestVersion, msgDHRs, msgPN, msgNs} = - smpEncode (msgVersion, msgLatestVersion, msgDHRs, msgPN, msgNs) + smpEncode MsgHeader {msgMaxVersion, msgDHRs, msgPN, msgNs} = + smpEncode (msgMaxVersion, msgDHRs, msgPN, msgNs) smpP = do - msgVersion <- smpP - msgLatestVersion <- smpP + msgMaxVersion <- smpP msgDHRs <- smpP msgPN <- smpP msgNs <- smpP - pure MsgHeader {msgVersion, msgLatestVersion, msgDHRs, msgPN, msgNs} + pure MsgHeader {msgMaxVersion, msgDHRs, msgPN, msgNs} -data EncHeader = EncHeader - { ehBody :: ByteString, - ehAuthTag :: AES.AuthTag, +data EncMessageHeader = EncMessageHeader + { ehVersion :: Version, + ehBody :: ByteString, + ehAuthTag :: AuthTag, ehIV :: IV } -serializeEncHeader :: EncHeader -> ByteString -serializeEncHeader EncHeader {ehBody, ehAuthTag, ehIV} = - ehBody <> authTagToBS ehAuthTag <> unIV ehIV +instance Encoding EncMessageHeader where + smpEncode EncMessageHeader {ehVersion, ehBody, ehAuthTag, ehIV} = + smpEncode (ehVersion, ehBody, ehAuthTag, ehIV) + smpP = do + ehVersion <- smpP + ehBody <- smpP + ehAuthTag <- smpP + ehIV <- smpP + pure EncMessageHeader {ehVersion, ehBody, ehAuthTag, ehIV} -encHeaderP :: Parser EncHeader -encHeaderP = do - ehBody <- A.take paddedHeaderLen - ehAuthTag <- bsToAuthTag <$> A.take authTagSize - ehIV <- ivP - pure EncHeader {ehBody, ehAuthTag, ehIV} - -data EncMessage = EncMessage +data EncRatchetMessage = EncRatchetMessage { emHeader :: ByteString, emBody :: ByteString, - emAuthTag :: AES.AuthTag + emAuthTag :: AuthTag } -serializeEncMessage :: EncMessage -> ByteString -serializeEncMessage EncMessage {emHeader, emBody, emAuthTag} = - emHeader <> emBody <> authTagToBS emAuthTag - -encMessageP :: Parser EncMessage -encMessageP = do - emHeader <- A.take fullHeaderLen - s <- A.takeByteString - when (B.length s <= authTagSize) $ fail "message too short" - let (emBody, aTag) = B.splitAt (B.length s - authTagSize) s - emAuthTag = bsToAuthTag aTag - pure EncMessage {emHeader, emBody, emAuthTag} +instance Encoding EncRatchetMessage where + smpEncode EncRatchetMessage {emHeader, emBody, emAuthTag} = + smpEncode (emHeader, emBody, emAuthTag) + smpP = do + emHeader <- smpP + emBody <- smpP + emAuthTag <- smpP + pure EncRatchetMessage {emHeader, emBody, emAuthTag} rcEncrypt' :: AlgorithmI a => Ratchet a -> Int -> ByteString -> ExceptT CryptoError IO (ByteString, Ratchet a) rcEncrypt' Ratchet {rcSnd = Nothing} _ _ = throwE CERatchetState -rcEncrypt' rc@Ratchet {rcSnd = Just sr@SndRatchet {rcCKs, rcHKs}, rcNs, rcAD} paddedMsgLen msg = do +rcEncrypt' rc@Ratchet {rcSnd = Just sr@SndRatchet {rcCKs, rcHKs}, rcNs, rcAD, rcVersion} paddedMsgLen msg = 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 -- return enc_header, ENCRYPT(mk, plaintext, CONCAT(AD, enc_header)) - let emHeader = serializeEncHeader EncHeader {ehBody, ehAuthTag, ehIV} + let emHeader = smpEncode EncMessageHeader {ehVersion = minVersion rcVersion, ehBody, ehAuthTag, ehIV} (emAuthTag, emBody) <- encryptAEAD mk iv paddedMsgLen (rcAD <> emHeader) msg - let msg' = serializeEncMessage EncMessage {emHeader, emBody, emAuthTag} + let msg' = smpEncode EncRatchetMessage {emHeader, emBody, emAuthTag} -- state.Ns += 1 rc' = rc {rcSnd = Just sr {rcCKs = ck'}, rcNs = rcNs + 1} pure (msg', rc') @@ -209,8 +210,7 @@ rcEncrypt' rc@Ratchet {rcSnd = Just sr@SndRatchet {rcCKs, rcHKs}, rcNs, rcAD} pa msgHeader = smpEncode MsgHeader - { msgVersion = rcVersion rc, - msgLatestVersion = currentE2EVersion, + { msgMaxVersion = maxVersion rcVersion, msgDHRs = fst $ rcDHRs rc, msgPN = rcPN rc, msgNs = rcNs @@ -236,8 +236,8 @@ rcDecrypt' :: ByteString -> ExceptT CryptoError IO (DecryptResult a) rcDecrypt' rc@Ratchet {rcRcv, rcMKSkipped, rcAD} msg' = do - encMsg@EncMessage {emHeader} <- parseE CryptoHeaderError encMessageP msg' - encHdr <- parseE CryptoHeaderError encHeaderP emHeader + encMsg@EncRatchetMessage {emHeader} <- parseE CryptoHeaderError smpP msg' + encHdr <- parseE CryptoHeaderError smpP emHeader -- plaintext = TrySkippedMessageKeysHE(state, enc_header, ciphertext, AD) decryptSkipped encHdr encMsg >>= \case SMNone -> do @@ -249,7 +249,7 @@ rcDecrypt' rc@Ratchet {rcRcv, rcMKSkipped, rcAD} msg' = do Nothing -> throwE CERatchetHeader SMMessage msg rc' -> pure (msg, rc') where - decryptRcMessage :: RatchetStep -> MsgHeader a -> EncMessage -> ExceptT CryptoError IO (DecryptResult a) + decryptRcMessage :: RatchetStep -> MsgHeader a -> EncRatchetMessage -> ExceptT CryptoError IO (DecryptResult a) decryptRcMessage rcStep MsgHeader {msgDHRs, msgPN, msgNs} encMsg = do -- if dh_ratchet: rc' <- ratchetStep rcStep @@ -310,7 +310,7 @@ rcDecrypt' rc@Ratchet {rcRcv, rcMKSkipped, rcAD} msg' = do let (ck', mk, iv, _) = chainKdf ck mks' = M.insert msgNs (MessageKey mk iv) mks in advanceRcvRatchet (n - 1) ck' (msgNs + 1) mks' - decryptSkipped :: EncHeader -> EncMessage -> ExceptT CryptoError IO (SkippedMessage a) + decryptSkipped :: EncMessageHeader -> EncRatchetMessage -> ExceptT CryptoError IO (SkippedMessage a) decryptSkipped encHdr encMsg = tryDecryptSkipped SMNone $ M.assocs rcMKSkipped where tryDecryptSkipped :: SkippedMessage a -> [(HeaderKey, SkippedMsgKeys)] -> ExceptT CryptoError IO (SkippedMessage a) @@ -335,7 +335,7 @@ rcDecrypt' rc@Ratchet {rcRcv, rcMKSkipped, rcAD} msg' = do msg <- decryptMessage mk encMsg pure $ SMMessage msg rc' tryDecryptSkipped r _ = pure r - decryptRcHeader :: Maybe RcvRatchet -> EncHeader -> ExceptT CryptoError IO (RatchetStep, MsgHeader a) + decryptRcHeader :: Maybe RcvRatchet -> EncMessageHeader -> ExceptT CryptoError IO (RatchetStep, MsgHeader a) decryptRcHeader Nothing hdr = decryptNextHeader hdr decryptRcHeader (Just RcvRatchet {rcHKr}) hdr = -- header = HDECRYPT(state.HKr, enc_header) @@ -344,11 +344,11 @@ rcDecrypt' rc@Ratchet {rcRcv, rcMKSkipped, rcAD} msg' = do e -> throwE e -- header = HDECRYPT(state.NHKr, enc_header) decryptNextHeader hdr = (AdvanceRatchet,) <$> decryptHeader (rcNHKr rc) hdr - decryptHeader k EncHeader {ehBody, ehAuthTag, ehIV} = do + decryptHeader k EncMessageHeader {ehBody, ehAuthTag, ehIV} = do header <- decryptAEAD k ehIV rcAD ehBody ehAuthTag `catchE` \_ -> throwE CERatchetHeader parseE' CryptoHeaderError smpP header - decryptMessage :: MessageKey -> EncMessage -> ExceptT CryptoError IO (Either CryptoError ByteString) - decryptMessage (MessageKey mk iv) EncMessage {emHeader, emBody, emAuthTag} = + decryptMessage :: MessageKey -> EncRatchetMessage -> ExceptT CryptoError IO (Either CryptoError ByteString) + decryptMessage (MessageKey mk iv) EncRatchetMessage {emHeader, emBody, emAuthTag} = -- DECRYPT(mk, ciphertext, CONCAT(AD, enc_header)) -- TODO add associated data tryE $ decryptAEAD mk iv (rcAD <> emHeader) emBody emAuthTag diff --git a/src/Simplex/Messaging/Encoding.hs b/src/Simplex/Messaging/Encoding.hs index dad328fba..e2f4486ab 100644 --- a/src/Simplex/Messaging/Encoding.hs +++ b/src/Simplex/Messaging/Encoding.hs @@ -7,7 +7,12 @@ {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-} -module Simplex.Messaging.Encoding (Encoding (..), Tail (..)) where +module Simplex.Messaging.Encoding + ( Encoding (..), + Tail (..), + Large (..), + ) +where import Data.Attoparsec.ByteString.Char8 (Parser) import qualified Data.Attoparsec.ByteString.Char8 as A @@ -16,6 +21,7 @@ import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.ByteString.Internal (c2w, w2c) import Data.Int (Int64) +import qualified Data.List.NonEmpty as L import Data.Time.Clock.System (SystemTime (..)) import Data.Word (Word16, Word32) import Network.Transport.Internal (decodeWord16, decodeWord32, encodeWord16, encodeWord32) @@ -64,15 +70,21 @@ w32P = fromIntegral <$> smpP @Word32 -- ByteStrings are assumed no longer than 255 bytes instance Encoding ByteString where - smpEncode s = B.cons (w2c len) s where len = fromIntegral $ B.length s - smpP = A.take . fromIntegral . c2w =<< A.anyChar + smpEncode s = B.cons (lenEncode $ B.length s) s + smpP = A.take =<< lenP + +lenEncode :: Int -> Char +lenEncode = w2c . fromIntegral + +lenP :: Parser Int +lenP = fromIntegral . c2w <$> A.anyChar instance Encoding a => Encoding (Maybe a) where - smpEncode s = maybe "\0" (("\1" <>) . smpEncode) s + smpEncode s = maybe "0" (("1" <>) . smpEncode) s smpP = smpP >>= \case - '\0' -> pure Nothing - '\1' -> Just <$> smpP + '0' -> pure Nothing + '1' -> Just <$> smpP _ -> fail "invalid Maybe tag" newtype Tail = Tail {unTail :: ByteString} @@ -81,6 +93,15 @@ instance Encoding Tail where smpEncode = unTail smpP = Tail <$> A.takeByteString +-- newtype for encoding/decoding ByteStrings over 255 bytes with 2-bytes length prefix +newtype Large = Large {unLarge :: ByteString} + +instance Encoding Large where + smpEncode (Large s) = smpEncode @Word16 (fromIntegral $ B.length s) <> s + smpP = do + len <- fromIntegral <$> smpP @Word16 + Large <$> A.take len + instance Encoding SystemTime where smpEncode = smpEncode . systemSeconds smpP = MkSystemTime <$> smpP <*> pure 0 @@ -89,6 +110,18 @@ instance (Encoding a, Encoding b) => Encoding (a, b) where smpEncode (a, b) = smpEncode a <> smpEncode b smpP = (,) <$> smpP <*> smpP +-- lists encode/parse as a sequence of items prefixed with list length (as 1 byte) +instance Encoding a => Encoding [a] where + smpEncode xs = B.cons (lenEncode $ length xs) . B.concat $ map smpEncode xs + smpP = (`A.count` smpP) =<< lenP + +instance Encoding a => Encoding (L.NonEmpty a) where + smpEncode = smpEncode . L.toList + smpP = + lenP >>= \case + 0 -> fail "empty list" + n -> L.fromList <$> A.count n smpP + instance (Encoding a, Encoding b, Encoding c) => Encoding (a, b, c) where smpEncode (a, b, c) = smpEncode a <> smpEncode b <> smpEncode c smpP = (,,) <$> smpP <*> smpP <*> smpP diff --git a/src/Simplex/Messaging/Encoding/String.hs b/src/Simplex/Messaging/Encoding/String.hs index cecb81802..f737e183a 100644 --- a/src/Simplex/Messaging/Encoding/String.hs +++ b/src/Simplex/Messaging/Encoding/String.hs @@ -16,6 +16,7 @@ import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.Char (isAlphaNum) import qualified Data.List.NonEmpty as L +import Data.Word (Word16) import Simplex.Messaging.Parsers (parseAll) import Simplex.Messaging.Util ((<$?>)) @@ -51,6 +52,10 @@ instance StrEncoding a => StrEncoding (Maybe a) where strEncode = maybe "" strEncode strP = optional strP +instance StrEncoding Word16 where + strEncode = B.pack . show + strP = A.decimal + -- lists encode/parse as comma-separated strings instance StrEncoding a => StrEncoding [a] where strEncode = B.intercalate "," . map strEncode @@ -58,12 +63,12 @@ instance StrEncoding a => StrEncoding [a] where instance StrEncoding a => StrEncoding (L.NonEmpty a) where strEncode = strEncode . L.toList - strP = - maybe (fail "empty list") pure . L.nonEmpty - =<< listItem `A.sepBy1'` A.char ',' + + -- relies on sepBy1 never returning an empty list + strP = L.fromList <$> listItem `A.sepBy1'` A.char ',' listItem :: StrEncoding a => Parser a -listItem = strDecode <$?> A.takeTill (== ',') +listItem = parseAll strP <$?> A.takeTill (== ',') instance (StrEncoding a, StrEncoding b) => StrEncoding (a, b) where strEncode (a, b) = B.unwords [strEncode a, strEncode b] diff --git a/src/Simplex/Messaging/Protocol.hs b/src/Simplex/Messaging/Protocol.hs index 27c766c98..e8d0d8729 100644 --- a/src/Simplex/Messaging/Protocol.hs +++ b/src/Simplex/Messaging/Protocol.hs @@ -49,7 +49,7 @@ module Simplex.Messaging.Protocol SignedTransmission, SentRawTransmission, SignedRawTransmission, - EncMessage (..), + ClientMsgEnvelope (..), PubHeader (..), ClientMessage (..), PrivHeader (..), @@ -97,7 +97,6 @@ import Data.Maybe (isNothing) import Data.String import Data.Time.Clock.System (SystemTime) import Data.Type.Equality -import Data.Word (Word16) import GHC.Generics (Generic) import Generic.Random (genericArbitraryU) import Network.Socket (HostName, ServiceName) @@ -317,15 +316,15 @@ instance ProtocolMsgTag BrokerMsgTag where _ -> Nothing -- | SMP message body format -data EncMessage = EncMessage - { emHeader :: PubHeader, - emNonce :: C.CbNonce, - emBody :: ByteString +data ClientMsgEnvelope = ClientMsgEnvelope + { cmHeader :: PubHeader, + cmNonce :: C.CbNonce, + cmEncBody :: ByteString } deriving (Show) data PubHeader = PubHeader - { phVersion :: Word16, + { phVersion :: Version, phE2ePubDhKey :: Maybe C.PublicKeyX25519 } deriving (Show) @@ -334,29 +333,30 @@ instance Encoding PubHeader where smpEncode (PubHeader v k) = smpEncode (v, k) smpP = PubHeader <$> smpP <*> smpP -instance Encoding EncMessage where - smpEncode EncMessage {emHeader, emNonce, emBody} = - smpEncode (emHeader, emNonce, Tail emBody) +instance Encoding ClientMsgEnvelope where + smpEncode ClientMsgEnvelope {cmHeader, cmNonce, cmEncBody} = + smpEncode (cmHeader, cmNonce, Tail cmEncBody) smpP = do - emHeader <- smpP - emNonce <- smpP - emBody <- A.takeByteString - pure EncMessage {emHeader, emNonce, emBody} + cmHeader <- smpP + cmNonce <- smpP + cmEncBody <- A.takeByteString + pure ClientMsgEnvelope {cmHeader, cmNonce, cmEncBody} data ClientMessage = ClientMessage PrivHeader ByteString data PrivHeader = PHConfirmation C.APublicVerifyKey | PHEmpty + deriving (Show) instance Encoding PrivHeader where smpEncode = \case PHConfirmation k -> "K" <> smpEncode k - PHEmpty -> " " + PHEmpty -> "_" smpP = A.anyChar >>= \case 'K' -> PHConfirmation <$> smpP - ' ' -> pure PHEmpty + '_' -> pure PHEmpty _ -> fail "invalid PrivHeader" instance Encoding ClientMessage where @@ -374,6 +374,13 @@ data SMPServer = SMPServer instance IsString SMPServer where fromString = parseString strDecode +instance Encoding SMPServer where + smpEncode SMPServer {host, port, keyHash} = + smpEncode (host, port, keyHash) + smpP = do + (host, port, keyHash) <- smpP + pure SMPServer {host, port, keyHash} + instance StrEncoding SMPServer where strEncode SMPServer {host, port, keyHash} = "smp://" <> strEncode keyHash <> "@" <> strEncode (SrvLoc host port) diff --git a/src/Simplex/Messaging/Transport.hs b/src/Simplex/Messaging/Transport.hs index a2f31710e..95a433f48 100644 --- a/src/Simplex/Messaging/Transport.hs +++ b/src/Simplex/Messaging/Transport.hs @@ -77,7 +77,6 @@ import Data.Default (def) import Data.Functor (($>)) import Data.Set (Set) import qualified Data.Set as S -import Data.Word (Word16) import qualified Data.X509 as X import qualified Data.X509.CertificateStore as XS import Data.X509.Validation (Fingerprint (..)) @@ -388,7 +387,7 @@ data THandle c = THandle { connection :: c, sessionId :: ByteString, -- | agreed SMP server protocol version - smpVersion :: Word16 + smpVersion :: Version } data ServerHandshake = ServerHandshake @@ -398,7 +397,7 @@ data ServerHandshake = ServerHandshake data ClientHandshake = ClientHandshake { -- | agreed SMP server protocol version - smpVersion :: Word16, + smpVersion :: Version, -- | server identity - CA certificate fingerprint keyHash :: C.KeyHash } @@ -495,7 +494,7 @@ clientHandshake c keyHash = do if sessionId /= sessId then throwE TEBadSession else case smpVersionRange `compatibleVersion` supportedSMPVersions of - Just smpVersion -> do + Just (Compatible smpVersion) -> do sendHandshake th $ ClientHandshake {smpVersion, keyHash} pure (th :: THandle c) {smpVersion} Nothing -> throwE $ TEHandshake VERSION diff --git a/src/Simplex/Messaging/Version.hs b/src/Simplex/Messaging/Version.hs index 829d65df1..96966bcda 100644 --- a/src/Simplex/Messaging/Version.hs +++ b/src/Simplex/Messaging/Version.hs @@ -1,41 +1,53 @@ +{-# LANGUAGE ConstrainedClassMethods #-} +{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeSynonymInstances #-} module Simplex.Messaging.Version - ( VersionRange (minVersion, maxVersion), + ( Version, + VersionRange (minVersion, maxVersion), pattern VersionRange, + VersionI (..), + VersionRangeI (..), + Compatible, + pattern Compatible, mkVersionRange, - versionRange, - compatibleVersion, + safeVersionRange, isCompatible, + proveCompatible, + compatibleVersion, ) where +import Control.Applicative (optional) import qualified Data.Attoparsec.ByteString.Char8 as A import Data.Word (Word16) import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Util (bshow) pattern VersionRange :: Word16 -> Word16 -> VersionRange pattern VersionRange v1 v2 <- VRange v1 v2 {-# COMPLETE VersionRange #-} +type Version = Word16 + data VersionRange = VRange - { minVersion :: Word16, - maxVersion :: Word16 + { minVersion :: Version, + maxVersion :: Version } deriving (Eq, Show) -- | construct valid version range, to be used in constants -mkVersionRange :: Word16 -> Word16 -> VersionRange +mkVersionRange :: Version -> Version -> VersionRange mkVersionRange v1 v2 | v1 <= v2 = VRange v1 v2 | otherwise = error "invalid version range" -versionRange :: Word16 -> Word16 -> Maybe VersionRange -versionRange v1 v2 +safeVersionRange :: Version -> Version -> Maybe VersionRange +safeVersionRange v1 v2 | v1 <= v2 = Just $ VRange v1 v2 | otherwise = Nothing @@ -43,18 +55,61 @@ instance Encoding VersionRange where smpEncode (VRange v1 v2) = smpEncode (v1, v2) smpP = maybe (fail "invalid version range") pure - =<< versionRange <$> smpP <*> smpP + =<< safeVersionRange <$> smpP <*> smpP instance StrEncoding VersionRange where - strEncode (VRange v1 v2) = bshow v1 <> "-" <> bshow v2 - strP = - maybe (fail "invalid version range") pure - =<< versionRange <$> A.decimal <* A.char '-' <*> A.decimal + strEncode (VRange v1 v2) + | v1 == v2 = strEncode v1 + | otherwise = strEncode v1 <> "-" <> strEncode v2 + strP = do + v1 <- strP + v2 <- maybe (pure v1) (const strP) =<< optional (A.char '-') + maybe (fail "invalid version range") pure $ safeVersionRange v1 v2 -compatibleVersion :: VersionRange -> VersionRange -> Maybe Word16 -compatibleVersion (VersionRange min1 max1) (VersionRange min2 max2) - | min1 <= max2 && min2 <= max1 = Just $ min max1 max2 - | otherwise = Nothing +class VersionI a where + type VersionRangeT a + version :: a -> Version + toVersionRangeT :: a -> VersionRange -> VersionRangeT a -isCompatible :: Word16 -> VersionRange -> Bool -isCompatible v (VersionRange v1 v2) = v1 <= v && v <= v2 +class VersionRangeI a where + type VersionT a + versionRange :: a -> VersionRange + toVersionT :: a -> Version -> VersionT a + +instance VersionI Version where + type VersionRangeT Version = VersionRange + version = id + toVersionRangeT _ vr = vr + +instance VersionRangeI VersionRange where + type VersionT VersionRange = Version + versionRange = id + toVersionT _ v = v + +newtype Compatible a = Compatible_ a + +pattern Compatible :: a -> Compatible a +pattern Compatible a <- Compatible_ a + +{-# COMPLETE Compatible #-} + +isCompatible :: VersionI a => a -> VersionRange -> Bool +isCompatible x (VRange v1 v2) = let v = version x in v1 <= v && v <= v2 + +isCompatibleRange :: VersionRangeI a => a -> VersionRange -> Bool +isCompatibleRange x (VRange min2 max2) = min1 <= max2 && min2 <= max1 + where + VRange min1 max1 = versionRange x + +proveCompatible :: VersionI a => a -> VersionRange -> Maybe (Compatible a) +proveCompatible x vr = x `mkCompatibleIf` (x `isCompatible` vr) + +compatibleVersion :: VersionRangeI a => a -> VersionRange -> Maybe (Compatible (VersionT a)) +compatibleVersion x vr = + toVersionT x (min max1 max2) `mkCompatibleIf` isCompatibleRange x vr + where + max1 = maxVersion $ versionRange x + max2 = maxVersion vr + +mkCompatibleIf :: a -> Bool -> Maybe (Compatible a) +x `mkCompatibleIf` cond = if cond then Just $ Compatible_ x else Nothing diff --git a/stack.yaml b/stack.yaml index 3ab1f0d98..f0d9da1b9 100644 --- a/stack.yaml +++ b/stack.yaml @@ -17,7 +17,7 @@ # # resolver: ./custom-snapshot.yaml # resolver: https://example.com/snapshots/2018-01-01.yaml -resolver: lts-18.13 +resolver: lts-18.21 # User packages to be built. # Various formats can be used as shown in the example below. @@ -36,9 +36,7 @@ packages: # extra-deps: - cryptostore-0.2.1.0@sha256:9896e2984f36a1c8790f057fd5ce3da4cbcaf8aa73eb2d9277916886978c5b19,3881 - - direct-sqlite-2.3.26@sha256:04e835402f1508abca383182023e4e2b9b86297b8533afbd4e57d1a5652e0c23,3718 - simple-logger-0.1.0@sha256:be8ede4bd251a9cac776533bae7fb643369ebd826eb948a9a18df1a8dd252ff8,1079 - - sqlite-simple-0.4.18.0@sha256:3ceea56375c0a3590c814e411a4eb86943f8d31b93b110ca159c90689b6b39e5,3002 # - ../hs-tls/core - github: simplex-chat/hs-tls commit: cea6d52c512716ff09adcac86ebc95bb0b3bb797 diff --git a/tests/AgentTests.hs b/tests/AgentTests.hs index 45ec2498d..9b3f0f80d 100644 --- a/tests/AgentTests.hs +++ b/tests/AgentTests.hs @@ -366,8 +366,6 @@ syntaxTests t = do describe "JOIN" $ do describe "valid" $ do - -- TODO: ERROR no connection alias in the response (it does not generate it yet if not provided) - -- TODO: add tests with defined connection alias it "using same server as in invitation" $ ( "311", "a", @@ -375,12 +373,12 @@ syntaxTests t = do <> urlEncode True "9VjLsOY5ZvB4hoglNdBzJFAUi_vP4GkZnJFahQOXV20=" <> "%40localhost%3A5001%2F3456-w%3D%3D%23" <> urlEncode True sampleDhKey - <> "&e2e=" + <> "&v=1" + <> "&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D" <> " 14\nbob's connInfo" ) >#> ("311", "a", "ERR SMP AUTH") describe "invalid" $ do - -- TODO: JOIN is not merged yet - to be added it "no parameters" $ ("321", "", "JOIN") >#> ("321", "", "ERR CMD SYNTAX") where -- simple test for one command with the expected response diff --git a/tests/AgentTests/ConnectionRequestTests.hs b/tests/AgentTests/ConnectionRequestTests.hs index 4b45b0d16..07fbf5f01 100644 --- a/tests/AgentTests/ConnectionRequestTests.hs +++ b/tests/AgentTests/ConnectionRequestTests.hs @@ -1,4 +1,5 @@ {-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} @@ -8,8 +9,10 @@ import Data.ByteString (ByteString) import Network.HTTP.Types (urlEncode) import Simplex.Messaging.Agent.Protocol import qualified Simplex.Messaging.Crypto as C +import Simplex.Messaging.Crypto.Ratchet (e2eEncryptVRange) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Protocol (smpClientVersion) +import Simplex.Messaging.Version import Test.Hspec uri :: String @@ -28,7 +31,7 @@ queue = SMPQueueUri { smpServer = srv, senderId = "\223\142z\251", - smpVersionRange = smpClientVersion, + clientVersionRange = smpClientVersion, dhPublicKey = testDhKey } @@ -41,37 +44,96 @@ testDhKeyStr = strEncode testDhKey testDhKeyStrUri :: ByteString testDhKeyStrUri = urlEncode True testDhKeyStr -connectionRequest :: AConnectionRequest +connReqData :: ConnReqUriData +connReqData = + ConnReqUriData + { crScheme = simplexChat, + crAgentVRange = smpAgentVRange, + crSmpQueues = [queue] + } + +testDhPubKey :: C.PublicKeyX448 +testDhPubKey = "MEIwBQYDK2VvAzkAmKuSYeQ/m0SixPDS8Wq8VBaTS1cW+Lp0n0h4Diu+kUpR+qXx4SDJ32YGEFoGFGSbGPry5Ychr6U=" + +testE2ERatchetParams :: E2ERatchetParamsUri +testE2ERatchetParams = E2ERatchetParamsUri e2eEncryptVRange testDhPubKey testDhPubKey + +testE2ERatchetParams13 :: E2ERatchetParamsUri +testE2ERatchetParams13 = E2ERatchetParamsUri (mkVersionRange 1 3) testDhPubKey testDhPubKey + +connectionRequest :: AConnectionRequestUri connectionRequest = - ACR SCMInvitation . CRInvitation $ - ConnReqData - { crScheme = simplexChat, - crSmpQueues = [queue], - crEncryption = ConnectionEncryption - } + ACRU SCMInvitation $ + CRInvitationUri connReqData testE2ERatchetParams + +connectionRequest12 :: AConnectionRequestUri +connectionRequest12 = + ACRU SCMInvitation $ + CRInvitationUri + connReqData {crAgentVRange = mkVersionRange 1 2, crSmpQueues = [queue, queue]} + testE2ERatchetParams13 connectionRequestTests :: Spec connectionRequestTests = describe "connection request parsing / serializing" $ do it "should serialize SMP queue URIs" $ do - strEncode queue {smpServer = srv {port = Nothing}} + strEncode (queue :: SMPQueueUri) {smpServer = srv {port = Nothing}} `shouldBe` "smp://1234-w==@smp.simplex.im/3456-w==#" <> testDhKeyStr - strEncode queue + strEncode queue {clientVersionRange = mkVersionRange 1 2} `shouldBe` "smp://1234-w==@smp.simplex.im:5223/3456-w==#" <> testDhKeyStr it "should parse SMP queue URIs" $ do + strDecode ("smp://1234-w==@smp.simplex.im/3456-w==#/?v=1&dh=" <> testDhKeyStr) + `shouldBe` Right (queue :: SMPQueueUri) {smpServer = srv {port = Nothing}} strDecode ("smp://1234-w==@smp.simplex.im/3456-w==#" <> testDhKeyStr) - `shouldBe` Right queue {smpServer = srv {port = Nothing}} + `shouldBe` Right (queue :: SMPQueueUri) {smpServer = srv {port = Nothing}} strDecode ("smp://1234-w==@smp.simplex.im:5223/3456-w==#" <> testDhKeyStr) `shouldBe` Right queue + strDecode ("smp://1234-w==@smp.simplex.im:5223/3456-w==#" <> testDhKeyStr <> "/?v=1&extra_param=abc") + `shouldBe` Right queue + strDecode ("smp://1234-w==@smp.simplex.im:5223/3456-w==#/?extra_param=abc&v=1-2&dh=" <> testDhKeyStr) + `shouldBe` Right queue {clientVersionRange = mkVersionRange 1 2} + strDecode ("smp://1234-w==@smp.simplex.im:5223/3456-w==#" <> testDhKeyStr <> "/?v=1-2&extra_param=abc") + `shouldBe` Right queue {clientVersionRange = mkVersionRange 1 2} it "should serialize connection requests" $ do strEncode connectionRequest - `shouldBe` "https://simplex.chat/invitation#/?smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23" + `shouldBe` "https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23" <> testDhKeyStrUri - <> "&e2e=" + <> "&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D" + strEncode connectionRequest12 + `shouldBe` "https://simplex.chat/invitation#/?v=1-2&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23" + <> testDhKeyStrUri + <> "%2Csmp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23" + <> testDhKeyStrUri + <> "&e2e=v%3D1-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D" it "should parse connection requests" $ do strDecode ( "https://simplex.chat/invitation#/?smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23" <> testDhKeyStrUri - <> "&e2e=" + <> "&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D" + <> "&v=1" ) `shouldBe` Right connectionRequest + strDecode + ( "https://simplex.chat/invitation#/?smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1%26dh%3D" + <> testDhKeyStrUri + <> "&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D" + <> "&v=1" + ) + `shouldBe` Right connectionRequest + strDecode + ( "https://simplex.chat/invitation#/?smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1%26dh%3D" + <> testDhKeyStrUri + <> "&e2e=v%3D1-1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D" + <> "&v=1-1" + ) + `shouldBe` Right connectionRequest + strDecode + ( "https://simplex.chat/invitation#/?smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1%26extra_param%3Dabc%26dh%3D" + <> testDhKeyStrUri + <> "%2Csmp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1%26dh%3D" + <> testDhKeyStrUri + <> "&e2e=extra_key%3Dnew%26v%3D1-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D" + <> "&some_new_param=abc" + <> "&v=1-2" + ) + `shouldBe` Right connectionRequest12 diff --git a/tests/AgentTests/DoubleRatchetTests.hs b/tests/AgentTests/DoubleRatchetTests.hs index be64dd0b6..e4e3508dd 100644 --- a/tests/AgentTests/DoubleRatchetTests.hs +++ b/tests/AgentTests/DoubleRatchetTests.hs @@ -40,12 +40,12 @@ paddedMsgLen :: Int paddedMsgLen = 100 fullMsgLen :: Int -fullMsgLen = fullHeaderLen + paddedMsgLen + C.authTagSize +fullMsgLen = 1 + fullHeaderLen + 1 + paddedMsgLen + C.authTagSize testMessageHeader :: Expectation testMessageHeader = do (k, _) <- C.generateKeyPair' @X25519 - let hdr = MsgHeader {msgVersion = 1, msgLatestVersion = 1, msgDHRs = k, msgPN = 0, msgNs = 0} + let hdr = MsgHeader {msgMaxVersion = e2eEncryptVersion, msgDHRs = k, msgPN = 0, msgNs = 0} parseAll (smpP @(MsgHeader 'X25519)) (smpEncode hdr) `shouldBe` Right hdr pattern Decrypted :: ByteString -> Either CryptoError (Either CryptoError ByteString) diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index b37d0cbd3..8053dd90b 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -42,9 +42,6 @@ functionalAPITests t = do withSmpServer t testAsyncInitiatingOffline it "should connect with joining client going offline before its queue activation" $ withSmpServer t testAsyncJoiningOfflineBeforeActivation - -- TODO a valid test case but not trivial to implement, probably requires some agent rework - xit "should connect with joining client going offline after its queue activation" $ - withSmpServer t testAsyncJoiningOfflineAfterActivation it "should connect with both clients going offline" $ withSmpServer t testAsyncBothOffline @@ -127,9 +124,6 @@ testAsyncJoiningOfflineBeforeActivation = do exchangeGreetings alice bobId bob' aliceId pure () -testAsyncJoiningOfflineAfterActivation :: IO () -testAsyncJoiningOfflineAfterActivation = error "not implemented" - testAsyncBothOffline :: IO () testAsyncBothOffline = do alice <- getSMPAgentClient cfg diff --git a/tests/AgentTests/SQLiteTests.hs b/tests/AgentTests/SQLiteTests.hs index 752dd944b..77a5f724b 100644 --- a/tests/AgentTests/SQLiteTests.hs +++ b/tests/AgentTests/SQLiteTests.hs @@ -97,10 +97,8 @@ storeTests = do describe "set Queue status" $ do describe "setRcvQueueStatus" $ do testSetRcvQueueStatus - testSetRcvQueueStatusNoQueue describe "setSndQueueStatus" $ do testSetSndQueueStatus - testSetSndQueueStatusNoQueue testSetQueueStatusDuplex describe "Msg management" $ do describe "create Msg" $ do @@ -377,18 +375,6 @@ testSetQueueStatusDuplex = getConn store "conn1" `returnsResult` SomeConn SCDuplex (DuplexConnection cData1 rcvQueue1 {status = Secured} sndQueue1 {status = Confirmed}) -testSetRcvQueueStatusNoQueue :: SpecWith SQLiteStore -testSetRcvQueueStatusNoQueue = - xit "should throw error on attempt to update status of non-existent RcvQueue" $ \store -> do - setRcvQueueStatus store rcvQueue1 Confirmed - `throwsError` SEConnNotFound - -testSetSndQueueStatusNoQueue :: SpecWith SQLiteStore -testSetSndQueueStatusNoQueue = - xit "should throw error on attempt to update status of non-existent SndQueue" $ \store -> do - setSndQueueStatus store sndQueue1 Confirmed - `throwsError` SEConnNotFound - hw :: ByteString hw = encodeUtf8 "Hello world!" @@ -406,13 +392,14 @@ mkRcvMsgData internalId internalRcvId externalSndId brokerId internalHash = sndMsgId = externalSndId, broker = (brokerId, ts) }, + msgType = A_MSG_, msgBody = hw, internalHash, externalPrevSndHash = "hash_from_sender" } -testCreateRcvMsg' :: SQLiteStore -> PrevExternalSndId -> PrevRcvMsgHash -> ConnId -> RcvMsgData -> Expectation -testCreateRcvMsg' st expectedPrevSndId expectedPrevHash connId rcvMsgData@RcvMsgData {..} = do +testCreateRcvMsg_ :: SQLiteStore -> PrevExternalSndId -> PrevRcvMsgHash -> ConnId -> RcvMsgData -> Expectation +testCreateRcvMsg_ st expectedPrevSndId expectedPrevHash connId rcvMsgData@RcvMsgData {..} = do let MsgMeta {recipient = (internalId, _)} = msgMeta updateRcvIds st connId `returnsResult` (InternalId internalId, internalRcvId, expectedPrevSndId, expectedPrevHash) @@ -426,8 +413,8 @@ testCreateRcvMsg = let ConnData {connId} = cData1 _ <- runExceptT $ createRcvConn st g cData1 rcvQueue1 SCMInvitation -- TODO getMsg to check message - testCreateRcvMsg' st 0 "" connId $ mkRcvMsgData (InternalId 1) (InternalRcvId 1) 1 "1" "hash_dummy" - testCreateRcvMsg' st 1 "hash_dummy" connId $ mkRcvMsgData (InternalId 2) (InternalRcvId 2) 2 "2" "new_hash_dummy" + testCreateRcvMsg_ st 0 "" connId $ mkRcvMsgData (InternalId $ -2) (InternalRcvId 1) 1 "1" "hash_dummy" + testCreateRcvMsg_ st 1 "hash_dummy" connId $ mkRcvMsgData (InternalId $ -1) (InternalRcvId 2) 2 "2" "new_hash_dummy" mkSndMsgData :: InternalId -> InternalSndId -> MsgHash -> SndMsgData mkSndMsgData internalId internalSndId internalHash = @@ -435,13 +422,14 @@ mkSndMsgData internalId internalSndId internalHash = { internalId, internalSndId, internalTs = ts, + msgType = A_MSG_, msgBody = hw, internalHash, prevMsgHash = internalHash } -testCreateSndMsg' :: SQLiteStore -> PrevSndMsgHash -> ConnId -> SndMsgData -> Expectation -testCreateSndMsg' store expectedPrevHash connId sndMsgData@SndMsgData {..} = do +testCreateSndMsg_ :: SQLiteStore -> PrevSndMsgHash -> ConnId -> SndMsgData -> Expectation +testCreateSndMsg_ store expectedPrevHash connId sndMsgData@SndMsgData {..} = do updateSndIds store connId `returnsResult` (internalId, internalSndId, expectedPrevHash) createSndMsg store connId sndMsgData @@ -454,8 +442,8 @@ testCreateSndMsg = let ConnData {connId} = cData1 _ <- runExceptT $ createSndConn store g cData1 sndQueue1 -- TODO getMsg to check message - testCreateSndMsg' store "" connId $ mkSndMsgData (InternalId 1) (InternalSndId 1) "hash_dummy" - testCreateSndMsg' store "hash_dummy" connId $ mkSndMsgData (InternalId 2) (InternalSndId 2) "new_hash_dummy" + testCreateSndMsg_ store "" connId $ mkSndMsgData (InternalId $ -2) (InternalSndId 1) "hash_dummy" + testCreateSndMsg_ store "hash_dummy" connId $ mkSndMsgData (InternalId $ -1) (InternalSndId 2) "new_hash_dummy" testCreateRcvAndSndMsgs :: SpecWith SQLiteStore testCreateRcvAndSndMsgs = @@ -464,9 +452,9 @@ testCreateRcvAndSndMsgs = let ConnData {connId} = cData1 _ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 SCMInvitation _ <- runExceptT $ upgradeRcvConnToDuplex store "conn1" sndQueue1 - testCreateRcvMsg' store 0 "" connId $ mkRcvMsgData (InternalId 1) (InternalRcvId 1) 1 "1" "rcv_hash_1" - testCreateRcvMsg' store 1 "rcv_hash_1" connId $ mkRcvMsgData (InternalId 2) (InternalRcvId 2) 2 "2" "rcv_hash_2" - testCreateSndMsg' store "" connId $ mkSndMsgData (InternalId 3) (InternalSndId 1) "snd_hash_1" - testCreateRcvMsg' store 2 "rcv_hash_2" connId $ mkRcvMsgData (InternalId 4) (InternalRcvId 3) 3 "3" "rcv_hash_3" - testCreateSndMsg' store "snd_hash_1" connId $ mkSndMsgData (InternalId 5) (InternalSndId 2) "snd_hash_2" - testCreateSndMsg' store "snd_hash_2" connId $ mkSndMsgData (InternalId 6) (InternalSndId 3) "snd_hash_3" + testCreateRcvMsg_ store 0 "" connId $ mkRcvMsgData (InternalId $ -2) (InternalRcvId 1) 1 "1" "rcv_hash_1" + testCreateRcvMsg_ store 1 "rcv_hash_1" connId $ mkRcvMsgData (InternalId $ -1) (InternalRcvId 2) 2 "2" "rcv_hash_2" + testCreateSndMsg_ store "" connId $ mkSndMsgData (InternalId 0) (InternalSndId 1) "snd_hash_1" + testCreateRcvMsg_ store 2 "rcv_hash_2" connId $ mkRcvMsgData (InternalId 1) (InternalRcvId 3) 3 "3" "rcv_hash_3" + testCreateSndMsg_ store "snd_hash_1" connId $ mkSndMsgData (InternalId 2) (InternalSndId 2) "snd_hash_2" + testCreateSndMsg_ store "snd_hash_2" connId $ mkSndMsgData (InternalId 3) (InternalSndId 3) "snd_hash_3" diff --git a/tests/CoreTests/VersionRangeTests.hs b/tests/CoreTests/VersionRangeTests.hs index d2d67b802..4a623cd87 100644 --- a/tests/CoreTests/VersionRangeTests.hs +++ b/tests/CoreTests/VersionRangeTests.hs @@ -1,5 +1,7 @@ {-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeFamilies #-} module CoreTests.VersionRangeTests where @@ -31,21 +33,26 @@ versionRangeTests = modifyMaxSuccess (const 1000) $ do (vr 1 3, vr 2 4) `compatible` Just 3 (vr 1 2, vr 3 4) `compatible` Nothing it "should check if version is compatible" $ do - isCompatible 1 (vr 1 2) `shouldBe` True - isCompatible 2 (vr 1 2) `shouldBe` True - isCompatible 2 (vr 1 1) `shouldBe` False - isCompatible 1 (vr 2 2) `shouldBe` False + isCompatible (1 :: Version) (vr 1 2) `shouldBe` True + isCompatible (2 :: Version) (vr 1 2) `shouldBe` True + isCompatible (2 :: Version) (vr 1 1) `shouldBe` False + isCompatible (1 :: Version) (vr 2 2) `shouldBe` False it "compatibleVersion should pass isCompatible check" . property $ \((min1, max1) :: (V, V)) ((min2, max2) :: (V, V)) -> min1 > max1 || min2 > max2 -- one of ranges is invalid, skip testing it || let w = fromIntegral . fromEnum - vr1 = mkVersionRange (w min1) (w max1) - vr2 = mkVersionRange (w min2) (w max2) + vr1 = mkVersionRange (w min1) (w max1) :: VersionRange + vr2 = mkVersionRange (w min2) (w max2) :: VersionRange in case compatibleVersion vr1 vr2 of - Just v -> v `isCompatible` vr1 && v `isCompatible` vr2 + Just (Compatible v) -> v `isCompatible` vr1 && v `isCompatible` vr2 _ -> True where vr = mkVersionRange + compatible :: (VersionRange, VersionRange) -> Maybe Version -> Expectation (vr1, vr2) `compatible` v = do - compatibleVersion vr1 vr2 `shouldBe` v - compatibleVersion vr2 vr1 `shouldBe` v + (vr1, vr2) `checkCompatible` v + (vr2, vr1) `checkCompatible` v + (vr1, vr2) `checkCompatible` v = + case compatibleVersion vr1 vr2 of + Just (Compatible v') -> Just v' `shouldBe` v + Nothing -> Nothing `shouldBe` v diff --git a/tests/SMPAgentClient.hs b/tests/SMPAgentClient.hs index a0003ce98..f3d952738 100644 --- a/tests/SMPAgentClient.hs +++ b/tests/SMPAgentClient.hs @@ -165,7 +165,7 @@ cfg = defaultTransport = (testPort, transport @TLS), tcpTimeout = 500_000 }, - retryInterval = (retryInterval defaultAgentConfig) {initialInterval = 50_000}, + reconnectInterval = (reconnectInterval defaultAgentConfig) {initialInterval = 50_000}, caCertificateFile = "tests/fixtures/ca.crt", privateKeyFile = "tests/fixtures/server.key", certificateFile = "tests/fixtures/server.crt"