From eb941f743511aad61bc0396a68dfe1d74060b0dd Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 19 Nov 2021 16:26:51 +0000 Subject: [PATCH 01/20] simplify pending message delivery (#202) * simplify pending message delivery (WIP) * refactor --- src/Simplex/Messaging/Agent.hs | 74 +++++++++------------ src/Simplex/Messaging/Agent/Client.hs | 9 +-- src/Simplex/Messaging/Agent/Store.hs | 2 +- src/Simplex/Messaging/Agent/Store/SQLite.hs | 4 +- 4 files changed, 37 insertions(+), 52 deletions(-) diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 18dd0d845..b539cf501 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -65,7 +65,6 @@ import Data.Composition ((.:), (.:.)) import Data.Functor (($>)) import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as L -import Data.Map.Strict (Map) import qualified Data.Map.Strict as M import Data.Maybe (isJust) import qualified Data.Text as T @@ -83,9 +82,9 @@ import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Protocol (MsgBody, SenderPublicKey) import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Transport (ATransport (..), TProxy, Transport (..), runTransportServer) -import Simplex.Messaging.Util (bshow, tryError) +import Simplex.Messaging.Util (bshow, tryError, unlessM) import System.Random (randomR) -import UnliftIO.Async (Async, async, race_) +import UnliftIO.Async (async, race_) import qualified UnliftIO.Exception as E import UnliftIO.STM @@ -297,7 +296,7 @@ subscribeConnection' :: forall m. AgentMonad m => AgentClient -> ConnId -> m () subscribeConnection' c connId = withStore (`getConn` connId) >>= \case SomeConn _ (DuplexConnection _ rq sq) -> do - resumeDelivery sq + resumeMsgDelivery c connId sq case status (sq :: SndQueue) of Confirmed -> withVerifyKey sq $ \verifyKey -> do conf <- withStore (`getAcceptedConfirmation` connId) @@ -308,7 +307,7 @@ subscribeConnection' c connId = Active -> subscribeQueue c rq connId _ -> throwError $ INTERNAL "unexpected queue status" SomeConn _ (SndConnection _ sq) -> do - resumeDelivery sq + resumeMsgDelivery c connId sq case status (sq :: SndQueue) of Confirmed -> withVerifyKey sq $ \verifyKey -> activateQueueJoining c connId sq verifyKey =<< resumeInterval @@ -316,12 +315,6 @@ subscribeConnection' c connId = _ -> throwError $ INTERNAL "unexpected queue status" SomeConn _ (RcvConnection _ rq) -> subscribeQueue c rq connId where - resumeDelivery :: SndQueue -> m () - resumeDelivery SndQueue {server} = do - wasDelivering <- resumeMsgDelivery c connId server - unless wasDelivering $ do - pending <- withStore (`getPendingMsgs` connId) - queuePendingMsgs c connId pending withVerifyKey :: SndQueue -> (C.PublicKey -> m ()) -> m () withVerifyKey sq action = let err = throwError $ INTERNAL "missing signing key public counterpart" @@ -344,14 +337,10 @@ sendMessage' c connId msg = _ -> throwError $ CONN SIMPLEX where enqueueMessage :: SndQueue -> m AgentMsgId - enqueueMessage SndQueue {server} = do + enqueueMessage sq@SndQueue {server} = do + resumeMsgDelivery c connId sq msgId <- storeSentMsg - wasDelivering <- resumeMsgDelivery c connId server - pending <- - if wasDelivering - then pure [PendingMsg {connId, msgId}] - else withStore (`getPendingMsgs` connId) - queuePendingMsgs c connId pending + queuePendingMsgs c connId server [msgId] pure $ unId msgId where storeSentMsg :: m InternalId @@ -372,42 +361,41 @@ sendMessage' c connId msg = createSndMsg st connId msgData pure internalId -resumeMsgDelivery :: forall m. AgentMonad m => AgentClient -> ConnId -> SMPServer -> m Bool -resumeMsgDelivery c connId srv = do - void $ resume srv (srvMsgDeliveries c) $ runSrvMsgDelivery c srv - resume connId (connMsgDeliveries c) $ runMsgDelivery c connId srv +resumeMsgDelivery :: forall m. AgentMonad m => AgentClient -> ConnId -> SndQueue -> m () +resumeMsgDelivery c connId SndQueue {server} = do + unlessM srvDelivering $ + async (runSrvMsgDelivery c server) + >>= atomically . modifyTVar (srvMsgDeliveries c) . M.insert server + unlessM connQueued $ + withStore (`getPendingMsgs` connId) + >>= queuePendingMsgs c connId server where - resume :: Ord a => a -> TVar (Map a (Async ())) -> m () -> m Bool - resume key actionMap actionProcess = do - isDelivering <- isJust . M.lookup key <$> readTVarIO actionMap - unless isDelivering $ - async actionProcess - >>= atomically . modifyTVar actionMap . M.insert key - pure isDelivering + srvDelivering = isJust . M.lookup server <$> readTVarIO (srvMsgDeliveries c) + connQueued = + atomically $ + isJust + <$> stateTVar + (connMsgsQueued c) + (\m -> (M.lookup connId m, M.insert connId True m)) -queuePendingMsgs :: AgentMonad m => AgentClient -> ConnId -> [PendingMsg] -> m () -queuePendingMsgs c connId pending = - atomically $ getPendingMsgQ connId (connMsgQueues c) >>= forM_ pending . writeTQueue +queuePendingMsgs :: AgentMonad m => AgentClient -> ConnId -> SMPServer -> [InternalId] -> m () +queuePendingMsgs c connId server msgIds = atomically $ do + q <- getPendingMsgQ c server + mapM_ (writeTQueue q . PendingMsg connId) msgIds -getPendingMsgQ :: Ord a => a -> TVar (Map a (TQueue PendingMsg)) -> STM (TQueue PendingMsg) -getPendingMsgQ key queueMap = do - maybe newMsgQueue pure . M.lookup key =<< readTVar queueMap +getPendingMsgQ :: AgentClient -> SMPServer -> STM (TQueue PendingMsg) +getPendingMsgQ c srv = do + maybe newMsgQueue pure . M.lookup srv =<< readTVar (srvMsgQueues c) where newMsgQueue :: STM (TQueue PendingMsg) newMsgQueue = do mq <- newTQueue - modifyTVar queueMap $ M.insert key mq + modifyTVar (srvMsgQueues c) $ M.insert srv mq pure mq -runMsgDelivery :: AgentMonad m => AgentClient -> ConnId -> SMPServer -> m () -runMsgDelivery c connId srv = do - mq <- atomically . getPendingMsgQ connId $ connMsgQueues c - smq <- atomically . getPendingMsgQ srv $ srvMsgQueues c - forever . atomically $ readTQueue mq >>= writeTQueue smq - runSrvMsgDelivery :: forall m. AgentMonad m => AgentClient -> SMPServer -> m () runSrvMsgDelivery c@AgentClient {subQ} srv = do - mq <- atomically . getPendingMsgQ srv $ srvMsgQueues c + mq <- atomically $ getPendingMsgQ c srv ri <- asks $ reconnectInterval . config forever $ do PendingMsg {connId, msgId} <- atomically $ readTQueue mq diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index cc9cfb340..031403a4a 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -72,8 +72,7 @@ data AgentClient = AgentClient subscrSrvrs :: TVar (Map SMPServer (Map ConnId RcvQueue)), subscrConns :: TVar (Map ConnId SMPServer), activations :: TVar (Map ConnId (Async ())), -- activations of send queues in progress - connMsgQueues :: TVar (Map ConnId (TQueue PendingMsg)), - connMsgDeliveries :: TVar (Map ConnId (Async ())), + connMsgsQueued :: TVar (Map ConnId Bool), srvMsgQueues :: TVar (Map SMPServer (TQueue PendingMsg)), srvMsgDeliveries :: TVar (Map SMPServer (Async ())), reconnections :: TVar [Async ()], @@ -93,14 +92,13 @@ newAgentClient agentEnv = do subscrSrvrs <- newTVar M.empty subscrConns <- newTVar M.empty activations <- newTVar M.empty - connMsgQueues <- newTVar M.empty - connMsgDeliveries <- newTVar M.empty + connMsgsQueued <- newTVar M.empty srvMsgQueues <- newTVar M.empty srvMsgDeliveries <- newTVar M.empty reconnections <- newTVar [] clientId <- stateTVar (clientCounter agentEnv) $ \i -> (i + 1, i + 1) lock <- newTMVar () - return AgentClient {rcvQ, subQ, msgQ, smpClients, subscrSrvrs, subscrConns, activations, connMsgQueues, connMsgDeliveries, srvMsgQueues, srvMsgDeliveries, reconnections, clientId, agentEnv, smpSubscriber = undefined, lock} + return AgentClient {rcvQ, subQ, msgQ, smpClients, subscrSrvrs, subscrConns, activations, connMsgsQueued, srvMsgQueues, srvMsgDeliveries, reconnections, clientId, agentEnv, smpSubscriber = undefined, lock} -- | Agent monad with MonadReader Env and MonadError AgentErrorType type AgentMonad m = (MonadUnliftIO m, MonadReader Env m, MonadError AgentErrorType m) @@ -176,7 +174,6 @@ closeAgentClient c = liftIO $ do closeSMPServerClients c cancelActions $ activations c cancelActions $ reconnections c - cancelActions $ connMsgDeliveries c cancelActions $ srvMsgDeliveries c closeSMPServerClients :: AgentClient -> IO () diff --git a/src/Simplex/Messaging/Agent/Store.hs b/src/Simplex/Messaging/Agent/Store.hs index fd8b3ced6..7ef0e7f1e 100644 --- a/src/Simplex/Messaging/Agent/Store.hs +++ b/src/Simplex/Messaging/Agent/Store.hs @@ -58,7 +58,7 @@ class Monad m => MonadAgentStore s m where createSndMsg :: s -> ConnId -> SndMsgData -> m () updateSndMsgStatus :: s -> ConnId -> InternalId -> SndMsgStatus -> m () getPendingMsgData :: s -> ConnId -> InternalId -> m (SndQueue, MsgBody) - getPendingMsgs :: s -> ConnId -> m [PendingMsg] + getPendingMsgs :: s -> ConnId -> m [InternalId] getMsg :: s -> ConnId -> InternalId -> m Msg checkRcvMsg :: s -> ConnId -> InternalId -> m () updateRcvMsgAck :: s -> ConnId -> InternalId -> m () diff --git a/src/Simplex/Messaging/Agent/Store/SQLite.hs b/src/Simplex/Messaging/Agent/Store/SQLite.hs index 5d63af27c..ec05920d6 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite.hs @@ -430,10 +430,10 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto sndQueue :: Maybe SndQueue -> Either StoreError SndQueue sndQueue = maybe (Left SEConnNotFound) Right - getPendingMsgs :: SQLiteStore -> ConnId -> m [PendingMsg] + getPendingMsgs :: SQLiteStore -> ConnId -> m [InternalId] getPendingMsgs st connId = liftIO . withTransaction st $ \db -> - map (PendingMsg connId . fromOnly) + map fromOnly <$> DB.query db "SELECT internal_id FROM snd_messages WHERE conn_alias = ? AND snd_status = ?" (connId, SndMsgCreated) getMsg :: SQLiteStore -> ConnId -> InternalId -> m Msg From eb7fcae31bab8323c77abfd7d703f20396845a3a Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 22 Nov 2021 08:52:39 +0000 Subject: [PATCH 02/20] update transport protocol to use TLS (#204) * update transport protocol to use TLS * typos * s/serverKeyHash/serverIdentity/ Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com> * Update protocol/simplex-messaging.md Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com> * corrections Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com> --- protocol/simplex-messaging.md | 109 +++++++++++++++++----------------- 1 file changed, 56 insertions(+), 53 deletions(-) diff --git a/protocol/simplex-messaging.md b/protocol/simplex-messaging.md index 7818773d6..a1b4ff5e9 100644 --- a/protocol/simplex-messaging.md +++ b/protocol/simplex-messaging.md @@ -72,7 +72,7 @@ The SMP model has three communication participants: the recipient, the message b SMP server manages multiple "simplex queues" - data records on the server that identify communication channels from the senders to the recipients. The same communicating party that is the sender in one queue, can be the recipient in another - without exposing this fact to the server. -The queue record consists of 2 unique random IDs generated by the server, one for the recipient and another for the sender, and 2 keys to authenticate the recipient and the sender respectively, provided by the client. The users of SMP protocol must use a unique key for each queue, to avoid the possibility of aggregating and analysing their queues in case SMP server is compromised. +The queue record consists of 2 unique random IDs generated by the server, one for the recipient and another for the sender, and 2 keys to authenticate the recipient and the sender respectively, provided by the client. The users of SMP protocol must use a unique key for each queue, to avoid the possibility of aggregating and analyzing their queues in case SMP server is compromised. Creating and using the queue requires sending commands to the SMP server from the recipient and the sender - they are described in detail in [SMP commands](#smp-commands) section. @@ -84,12 +84,15 @@ The [ABNF][8] syntax of the message is: ```abnf queueInfo = %s"smp::" smpServer "::" queueId "::" encryptionKey -smpServer = srvHost [":" port] ["#" serverKeyHash] +smpServer = srvHost [":" port] "#" serverIdentity srvHost = ; RFC1123, RFC5891 port = 1*DIGIT -serverKeyHash = encoded +serverIdentity = encoded queueId = encoded -encryptionKey = %s"rsa:" x509encoded ; the recipient's RSA public key for sender to encrypt messages +encryptionKey = encryptionScheme ":" x509encoded ; the recipient's RSA public key for sender to encrypt messages +encryptionScheme = %s"rsa" ; end-to-end encryption and key exchange protocols, + ; the current hybrid encryption scheme (RSA-OAEP/AES-256-GCM-SHA256) + ; will be replaced with double ratchet protocol and DH key exchange. x509encoded = encoded = ``` @@ -98,7 +101,7 @@ encoded = `port` is optional, the default TCP port for SMP protocol is 5223. -`serverKeyHash` is an optional hash of the server transport key used during transport handshake (see [Appendix A](#appendix-a)). +`serverIdentity` is a required hash of the server certificate SPKI block (without line breaks, header and footer) used by the client to validate server certificate during transport handshake (see [Appendix A](#appendix-a)). Encryption keys are encoded using [X509][11] specification. @@ -272,7 +275,7 @@ Simplex Messaging Protocol: - One unique "public" key is used by the servers to authenticate requests to send the messages into the queue, and another unique "public" key - to retrieve the messages from the queue. "Unique" here means that each "public" key is used only for one queue and is not used for any other context - effectively, this key is not public and does not represent any participant identity. - - Both recipient and sender "public" keys are provided to the server by the queue recipient. "Public" key `RK` is provided when the queue is created, public key `SK` is proviced when the queue is secured. + - Both recipient and sender "public" keys are provided to the server by the queue recipient. "Public" key `RK` is provided when the queue is created, public key `SK` is provided when the queue is secured. - The "public" keys known to the server and used to authenticate commands from the participants are unrelated to the keys used to encrypt and decrypt the messages - the latter keys are also unique per each queue but they are only known to participants, not to the servers. @@ -348,16 +351,18 @@ Each transmission between the client and the server must have this format/syntax ```abnf transmission = [signature] SP signed SP pad ; pad to the fixed block size -signed = [corrId] SP [queueId] SP cmd +signed = sessionIdentifier SP [corrId] SP [queueId] SP cmd ; corrId is required in client commands and server responses, + ; corrId is empty in server notifications. cmd = ping / recipientCmd / send / subscribeNotifications / serverMsg recipientCmd = create / subscribe / secure / enableNotifications / acknowledge / suspend / delete -serverMsg = pong / queueIds / message / notifierId / messageNotification / +serverMsg = queueIds / message / notifierId / messageNotification / unsubscribed / ok / error corrId = 1*(%x21-7F) ; any characters other than control/whitespace queueId = encoded ; empty queue ID is used with "create" command signature = encoded -; empty signature can be used with "create", "send" and "ping" commands and server messages +; empty signature can be used with "send" before the queue is secured with secure command +; signature is always empty with "ping" and "serverMsg" encoded = ``` @@ -367,21 +372,20 @@ The syntax of specific commands and responses is defined below. ### Correlating responses with commands -The server should send `queueIds`, `error` and `ok` responses in the same order within each queue ID as the commands received in the transport connection, so that they can be correlated by the clients. To simplify correlation of commands and responses, the server should use the same `corrId` in the response as in the command sent by the client. +The server should send `queueIds`, `error` and `ok` responses in the same order within each queue ID as the commands received in the transport connection, so that they can be correlated by the clients. To simplify correlation of commands and responses, the server must use the same `corrId` in the response as in the command sent by the client. If the transport connection is closed before some responses are sent, these responses should be discarded. ### Command authentication -SMP servers must authenticate all transmissions (excluding `ping` and `send` commands) by verifying the provided signatures. Command signature should be generated by applying RSA-PSS algorithm to the `signed` block of the transmission using the key associated with the queue ID (sender's or recipient's, depending on which queue ID is used). +SMP servers must authenticate all transmissions (excluding `ping` and initial `send` commands) by verifying the client signatures. Command signature should be generated by applying the algorithm specified for the queue to the `signed` block of the transmission, using the key associated with the queue ID (recipient's, sender's or notifier's, depending on which queue ID is used). ### Keep-alive command -To keep the transport connection alive and to generate noise traffic the clients should use `ping` command to which the server responds with `pong` response. This command should be sent unsigned and without queue ID. +To keep the transport connection alive and to generate noise traffic the clients should use `ping` command to which the server responds with `ok` response. This command should be sent unsigned and without queue ID. ```abnf ping = %s"PING" -pong = %s"PONG" ``` ### Recipient commands @@ -394,7 +398,9 @@ This command is sent by the recipient to the SMP server to create a new queue. T ```abnf create = %s"NEW" SP recipientKey -recipientKey = %s"rsa:" x509encoded ; the recipient's RSA public key for this queue +recipientKey = signatureScheme ":" x509encoded ; the recipient's public key to verify commands for this queue +signatureScheme = %s"rsa" | %s"ed25519" | %s"ed448" ; "rsa" means deprecated RSA-PSS signature scheme, + ; it must not be used for the new queues. x509encoded = ``` @@ -410,7 +416,7 @@ This response should be sent with empty queue ID (the second part of the transmi Once the queue is created, the recipient gets automatically subscribed to receive the messages from that queue, until the transport connection is closed. The `subscribe` command is needed only to start receiving the messages from the existing queue when the new transport connection is opened. -NEW `transmission` must be signed using the `recipientKey` that was passed in the transmission. +NEW `transmission` must be signed using the `recipientKey` that was passed in the transmission – this verifies that the client has the private key that will be used to sign subsequent commands for this queue. #### Subscribe to queue @@ -430,7 +436,7 @@ This command is sent by the recipient to the server to add sender's key to the q ```abnf secure = %s"KEY" SP senderKey -senderKey = %s"rsa:" x509encoded ; the sender's RSA public key for this queue +senderKey = signatureScheme ":" x509encoded ; the sender's public key public key to verify SEND command for this queue ``` `senderKey` is received from the sender as part of the first message - see [Send Message](#send-message) command. @@ -443,7 +449,7 @@ This command is sent by the recipient to the server to add notifier's key to the ```abnf enableNotifications = %s"NKEY" SP notifierKey -notifierKey = %s"rsa:" x509encoded ; the notifier's RSA public key for this queue +notifierKey = signatureScheme ":" x509encoded ; the notifier's public key public key to verify NSUB command for this queue ``` The server will respond with `notifierId` response if notifications were enabled and the notifier's key was successfully added to the queue: @@ -528,7 +534,7 @@ The body should be encrypted with the recipient's "public" key (`EK`); once decr decryptedBody = [clientHeader] CRLF clientBody CRLF clientHeader = senderKeyMsg senderKeyMsg = %s"KEY" SP senderKey -senderKey = %s"rsa:" x509encoded ; the sender's RSA public key for this queue +senderKey = signatureScheme ":" x509encoded ; the sender's public key to sign SEND commands for this queue clientBody = *OCTET ``` @@ -570,7 +576,7 @@ timestamp = `timestamp` - the UTC time when the server received the message from the sender, must be in date-time format defined by [RFC 3339][10] -`binaryMsg` - see syntax in [Send message](#send-message) +`msgBody` - see syntax in [Send message](#send-message) #### Notifier queue ID response @@ -639,52 +645,47 @@ ok = %s"OK" Both the recipient and the sender can use TCP or some other, possibly higher level, transport protocol to communicate with the server. The default TCP port for SMP server is 5223. -By default, the client and server should use the protocol presented below, that does not depend on a centralized certificate authority. +For scenarios when meta-data privacy is critical, it is recommended that clients: +- communicating over Tor network, +- establish a separate connection for each SMP queue, +- send noise traffic (using PING command). -Transport is encrypted with [AEAD-GCM][12] protocol with two random symmetric AES 256-bit keys and two random base IVs that will be agreed during the handshake. Both client and the server should maintain two 32-bit word counters, one for the sent and one for the received messages. The IV for each message should be computed by xor-ing the sequential message counter, starting from 0, with the first 32 bits of agreed base IV (the number is encoded in network byte order). +In addition to that, the servers can be deployed as Tor onion services. -To establish the session keys and base IVs, the server should have an asymmetric key pair generated during server deployment and unknown to the clients. The users should know the key hash (256 bits) in advance in order to be able to validate the server public key during transport connection handshake. +The transport protocol should provide the following: +- server authentication (by matching server certificate hash with `serverIdentity`), +- forward secrecy (by encrypting the traffic using ephemeral keys agreed during transport handshake), +- integrity (preventing data modification by the attacker without detection), +- unique channel binding (`sessionIdentifier`) to include in the signed part of SMP transmissions. -The handshake sequence is the following: +By default, the client and server communicate using [TLS 1.3 protocol][13] restricted to: +- TLS_AES_256_GCM_SHA384 cypher suite, +- ed25519 and ed448 EdDSA algorithms for signatures, +- x25519 and x448 ECDHE groups for key exchange. +- servers must send only one self-signed certificate in the handshake, clients must abort the connection in case more than one certificate is sent. +- server and client TLS configuration should not allow resuming the sessions. -1. Once the connection is established, the server sends the `server_header` followed by its public RSA key encoded in X509 binary (not base-64 encoded) format to the client. -2. The client compares the SHA256 hash of the received key with the hash it already has (e.g. received as part of connection invitation or as SMP server configuration). If the hash does not match, the client must terminate the connection. -3. If the hash is the same, the client should generate two random symmetric 256-bit AES keys and two base IVs that will be used as session keys/IVs by the client and the server. -4. The client then should create the `client_handshake` block and send it to the server, encrypted using [RSA-OAEP][2] scheme with the server public key: `rsa-encrypt(client_handshake)`. `snd_aes_key` and `snd_base_iv` will be used by the client to encrypt **sent** messages and by the server to decrypt them, `rcv_aes_key` and `rcv_base_iv` will be used by the client to decrypt **received** messages and by the server to encrypt them. `client_handshake` also contains `block_size` and reserved `protocol` blocks (see syntax). -5. The server should decrypt the received AES keys and base IVs with its private RSA key. -6. In case of successful decryption, the server should send encrypted welcome block (`encrypted_welcome_block`) that contains SMP protocol version supported by the server. +During TLS handshake the client must validate that the hash of the server certificate SPKI block is equal to the `serverIdentity` the client received as part of SMP server address; if the server identity does not match the client must abort the connection. -All the subsequent data, both from the client and from the server, should be sent padded to the fixed agreed block size, encrypted with symmetric AES keys and base IVs (incremented by counters on both sides), that were sent by the client during the handshake. If the application needs to transmit a larger message, it should be broken down into fragments. +Once TLS handshake is complete, client and server will exchange blocks of fixed size (16384 bytes). -Handshake blocks sent by the client and the server have this syntax: +The first block sent by the client should be `clientHello` and the server should respond with `serverHello`: ```abnf -server_header = block_size protocol key_size -block_size = 4*4(OCTET) ; 4-byte block size sent by the server -protocol = 2*2(%x00) ; 0, reserved -key_size = 2*2(OCTET) ; the size of the encoded key in bytes (binary encoded in X509 standard) +clientHello = SP smpVersion SP reserved pad +serverHello = sessionIdentifier SP smpVersion SP reserved pad +sessionIdentifier = ; unique session identifier derived from transport connection handshake + ; it should be included in all SMP transmissions sent in this transport connection. -client_handshake = client_block_size protocol snd_aes_key snd_base_iv rcv_aes_key rcv_base_iv -client_block_size = 4*4(OCTET) ; 4-byte block size sent by the client, -; to confirm or override the block size sent by the server -client_protocol = 2*2(%x00) ; 0, reserved -snd_aes_key = 32*32(OCTET) -snd_base_iv = 16*16(OCTET) -rcv_aes_key = 32*32(OCTET) -rcv_base_iv = 16*16(OCTET) - -transport_block = aes_body_auth_tag aes_encrypted_body -; size is sent by server during handshake, usually 4096 bytes -aes_body_auth_tag = 16*16(OCTET) -aes_encrypted_body = 1*OCTET - -encrypted_welcome_block = transport_block -welcome_block = smp_version SP pad ; decrypt(encrypted_welcome_block) -smp_version = %s"v" 1*DIGIT "." 1*DIGIT "." 1*DIGIT ["-" 1*ALPHA "." 1*DIGIT] ; in semver format - ; for example: v123.456.789-alpha.7 +smpVersion = %s"SMP v" 1*DIGIT "." 1*DIGIT "." 1*DIGIT ; semver format, the version in this document is v0.5.0 +reserved = pad = 1*OCTET ``` +For TLS 1.3 transport client should assert that `sessionIdentifier` is equal to `tls-unique` channel binding defined in [RFC 5929][14] (TLS Finished message struct); we pass it in `serverHello` block to allow communication over some other transport protocol. + +The communication party (client or server) that has the lower protocol version should assume that this version will be supported by another party, the party with the higher protocol version should abort the connection in case they cannot support the lower version. + [1]: https://en.wikipedia.org/wiki/Man-in-the-middle_attack [2]: https://en.wikipedia.org/wiki/End-to-end_encryption [3]: https://en.wikipedia.org/wiki/QR_code @@ -697,3 +698,5 @@ pad = 1*OCTET [10]: https://tools.ietf.org/html/rfc3339 [11]: https://tools.ietf.org/html/rfc5280 [12]: https://tools.ietf.org/html/rfc7714 +[13]: https://datatracker.ietf.org/doc/html/rfc8446 +[14]: https://datatracker.ietf.org/doc/html/rfc5929#section-3 From 01e8c232f03d262c511c98ec1fda8cda53b15db6 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 22 Nov 2021 19:08:19 +0000 Subject: [PATCH 03/20] change syntax for queue URI and connection request (#205) * change syntax for queue URI and connection request * separate queue URI into separate section * correction --- protocol/agent-protocol.md | 53 ++++++++++++++++++++--------------- protocol/simplex-messaging.md | 52 +++++++++++++++++----------------- protocol/smp-uri-request.txt | 16 +++++++++++ 3 files changed, 72 insertions(+), 49 deletions(-) create mode 100644 protocol/smp-uri-request.txt diff --git a/protocol/agent-protocol.md b/protocol/agent-protocol.md index 9d80beef0..417a75853 100644 --- a/protocol/agent-protocol.md +++ b/protocol/agent-protocol.md @@ -24,7 +24,7 @@ - [END notification](#end-notification) - [OFF command](#off-command) - [DEL command](#del-command) -- [Connection invitation](#connection-invitation) +- [Connection request](#connection-request) ## Abstract @@ -32,28 +32,27 @@ The purpose of SMP agent protocol is to define the syntax and the semantics of c It provides: - convenient protocol to create and manage bi-directional (duplex) connections between the users of SMP agents consisting of two (or more) separate unidirectional (simplex) SMP queues, abstracting away multiple steps required to establish bi-directional connections and any information about the servers location from the users of the protocol. -- management of E2E encryption between SMP agents, generating ephemeral RSA keys for each connection. -- SMP command authentication on SMP servers, generating ephemeral RSA keys for each SMP queue. -- TCP transport handshake and encryption with SMP servers. +- management of E2E encryption between SMP agents, generating ephemeral asymmetric keys for each connection. +- SMP command authentication on SMP servers, generating ephemeral keys for each SMP queue. +- TCP/TLS transport handshake with SMP servers. - validation of message integrity. -SMP agent protocols provides no encryption or any security on the client side - it is assumed that the agent is executed in the trusted and secure environment. +SMP agent protocol provides no encryption or security on the client side - it is assumed that the agent is executed in the trusted and secure environment, in one of three ways: +- via TCP network using secure connection. +- via local port (when the agent runs on the same device as a separate process). +- via agent library, when the agent logic is included directly into the client application. -The future versions of this protocol could provide: -- managing redundant SMP queues with more than 1 queue in each direction. -- managing simple symmetric groups as a foundation for chat groups and device synchronization. -- agent cluster - synchronizing states of multiple agents. -- secure "synchronous" streams with symmetric message encryption and connection-level authentication (requires extending [SMP protocol](./simplex-messaging.md)) - it can be used, e.g., for file transfers. +The last option is the most secure, as it reduces the number of attack vectors in comparison with other options. [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat) uses this approach. ## SMP agent -SMP agent is a client-side process or library that communicates via SMP servers using [simplex messaging protocol (SMP)](./simplex-messaging.md) with other SMP agents according to the commands received from its users. This protocol is a middle layer in SMP protocols stack (above SMP protocol but below any application level protocol) - it is intended to be used by client-side applications that need secure asynchronous bi-directional communication channels ("connections"). +SMP agent communicates via SMP servers using [simplex messaging protocol (SMP)](./simplex-messaging.md) with other SMP agents according to the commands received from its users. This protocol is a middle layer in SimpleX protocols (above SMP protocol but below any application level protocol) - it is intended to be used by client-side applications that need secure asynchronous bi-directional communication channels ("connections"). -The agent must have a persistent storage to manage the states of known connections and of the client-side information of two SMP queues that each connection consists of, and also the buffer of the most recent messages. The number of the messages that should be stored is implementation specific, depending on the error management approach that the agent implements; at the very least the agent must store the hash and id of the last received message. +The agent must have a persistent storage to manage the states of known connections and of the client-side information of SMP queues that each connection consists of, and also the buffer of the most recent sent and received messages. The number of the messages that should be stored is implementation specific, depending on the error management approach that the agent implements; at the very least the agent must store the hash and id of the last received and sent message. ## SMP servers management -SMP agent protocol commands do not contain SMP servers that the agent will use to establish the connections between their users. The servers are part of the agent configuration and can be dynamically added and removed by the agent implementation: +SMP agent protocol commands do not contain SMP servers that the agent will use to establish the connections. The servers are part of the agent configuration and can be dynamically added and removed by the agent implementation: - by the client applications via any API that is outside of scope of this protocol. - by the agents themselves based on servers availability and latency. @@ -64,7 +63,7 @@ SMP agent protocol has 3 main parts: - the syntax and semantics of messages that SMP agents exchange between each other in order to: - negotiate establishing unidirectional (simplex) encrypted queues on SMP server(s) - exchange client messages and delivery notifications, providing sequential message IDs and message integrity (by including the hash of the previous message). -- the syntax and semantics of the commands (a higher level interface than SMP protocol) that are sent over TCP or other sequential protocol by agent clients to the agents. This protocol allows to create and manage multiple connections, each consisting of two simplex SMP queues. +- the syntax and semantics of the commands that are sent over TCP or other sequential protocol by agent clients to the agents. This protocol allows to create and manage multiple connections, each consisting of two or more SMP queues. - the syntax and semantics of the message that the clients of SMP agents should send out-of-band (as pre-shared "invitation" including SMP server, queue ID and encryption key) to ensure [E2E encryption][1] the integrity of SMP queues and protection against active attacks ([MITM attacks][2]). ## Duplex connection procedure @@ -74,7 +73,7 @@ SMP agent protocol has 3 main parts: The procedure of establishing a duplex connection is explained on the example of Alice and Bob creating a bi-directional connection comprised of two unidirectional (simplex) queues, using SMP agents (A and B) to facilitate it, and two different SMP servers (which could be the same server). It is shown on the diagram above and has these steps: 1. Alice requests the new connection from the SMP agent A using `NEW` command. -2. Agent A creates an SMP connection on the server (using [SMP protocol](./simplex-messaging.md)) and responds to Alice with the invitation that contains queue information and the encryption key Bob's agent B should use. The invitation format is described in [Connection invitation](#connection-invitation). +2. Agent A creates an SMP connection on the server (using [SMP protocol](./simplex-messaging.md)) and responds to Alice with the invitation that contains queue information and the encryption key Bob's agent B should use. The invitation format is described in [Connection request](#connection-request). 3. Alice sends the invitation to Bob via any secure channel they have (out-of-band message). 4. Bob sends `JOIN` command with the invitation as a parameter to agent B to accept the connection. 5. Establishing Alice's SMP connection (with SMP protocol commands): @@ -138,7 +137,7 @@ helloMsg = %s"HELLO" SP signatureVerificationKey [SP %s"NO_ACK"] ; NO_ACK means that acknowledgements to client messages will NOT be sent in this connection by the agent that sent `HELLO` message. signatureVerificationKey = encoded -replyQueueMsg = %s"REPLY" SP ; `queueInfo` is the same as in out-of-band message, see SMP protocol +replyQueueMsg = %s"REPLY" SP ; `connectionRequest` is defined below ; this message can only be sent by the second connection party deleteQueueMsg = %s"DEL" ; notification that recipient queue will be deleted @@ -204,7 +203,7 @@ agentMsg = invitation / connRequest / connInfo / connected / unsubscribed / conn newCmd = %s"NEW" [SP %s"NO_ACK"] ; response is `invitation` or `error` ; NO_ACK parameter currently not supported -invitation = %s"INV" SP ; `queueInfo` is the same as in out-of-band message, see SMP protocol +invitation = %s"INV" SP ; `connectionRequest` is defined below connRequest = %s"REQ" SP confirmationId SP msgBody ; msgBody here is any binary information identifying connection request @@ -231,8 +230,8 @@ connDown = %s"DOWN" connUp = %s"UP" ; restored connection -joinCmd = %s"JOIN" SP [SP %s"NO_REPLY"] [SP %s"NO_ACK"] -; `queueInfo` is the same as in out-of-band message, see SMP protocol +joinCmd = %s"JOIN" SP [SP %s"NO_REPLY"] [SP %s"NO_ACK"] +; `connectionRequest` is defined below ; response is `connected` or `error` suspendCmd = %s"OFF" ; can be sent by either party, response `ok` or `error` @@ -348,11 +347,21 @@ It is used to suspend the receiving SMP queue - sender will no longer be able to It is used to delete the connection and all messages in it, as well as the receiving SMP queue and all messages in it that were remaining on the server. Agent response to this command can be `OK` or `ERR`. This command is irreversible. -## Connection invitation +## Connection request -Connection invitation `queueInfo` is generated by SMP agent in response to `newCmd` command (`"NEW"`), used by another party user with `joinCmd` command (`"JOIN"`), and then another invitation is sent by the agent in `replyQueueMsg` and used by the first party agent to connect to the reply queue (the second part of the process is invisible to the users). +Connection request `connectionRequest` is generated by SMP agent in response to `newCmd` command (`"NEW"`), used by another party user with `joinCmd` command (`"JOIN"`), and then another invitation is sent by the agent in `replyQueueMsg` and used by the first party agent to connect to the reply queue (the second part of the process is invisible to the users). -See SMP protocol [out-of-band messages](./simplex-messaging.md#out-of-band-messages) for connection invitation syntax. +Connection request syntax: + +``` +connectionRequest = queueURI "#/connect/" encryptionScheme ":" publicKey +encryptionScheme = %s"rsa" ; end-to-end encryption and key exchange protocols, + ; the current hybrid encryption scheme (RSA-OAEP/AES-256-GCM-SHA256) + ; will be replaced with double ratchet protocol and DH key exchange. +publicKey = +``` + +See SMP protocol [out-of-band messages](./simplex-messaging.md#out-of-band-messages) for syntax for queueURI. [1]: https://en.wikipedia.org/wiki/End-to-end_encryption [2]: https://en.wikipedia.org/wiki/Man-in-the-middle_attack diff --git a/protocol/simplex-messaging.md b/protocol/simplex-messaging.md index a1b4ff5e9..ad08ccb89 100644 --- a/protocol/simplex-messaging.md +++ b/protocol/simplex-messaging.md @@ -7,6 +7,7 @@ - [SMP Model](#smp-model) - [Out-of-band messages](#out-of-band-messages) - [Simplex queue](#simplex-queue) +- [SMP queue URI](#smp-queue-uri) - [SMP procedure](#smp-procedure) - [SMP qualities and features](#smp-qualities-and-features) - [Cryptographic algorithms](#cryptographic-algorithms) @@ -78,34 +79,9 @@ Creating and using the queue requires sending commands to the SMP server from th ## Out-of-band messages -The out-of-band message with the queue information is sent via some trusted alternative channel from the recipient to the sender. This message is used to share the encryption (a.k.a. "public") key that the sender will use to encrypt the messages (to be decrypted by the recipient), sender queue ID, server hostname and any other information necessary to establish secure encrypted connection with SMP server (see [Appendix A](#appendix-a) for SMP transport protocol). +The out-of-band message with the queue information is sent via some trusted alternative channel from the recipient to the sender. This message is used to share one or several [queue URIs](#smp-queue-uri) that parties can use to establish the initial connection, the encryption scheme and, it can include the public key(s) for end-to-end encryption. -The [ABNF][8] syntax of the message is: - -```abnf -queueInfo = %s"smp::" smpServer "::" queueId "::" encryptionKey -smpServer = srvHost [":" port] "#" serverIdentity -srvHost = ; RFC1123, RFC5891 -port = 1*DIGIT -serverIdentity = encoded -queueId = encoded -encryptionKey = encryptionScheme ":" x509encoded ; the recipient's RSA public key for sender to encrypt messages -encryptionScheme = %s"rsa" ; end-to-end encryption and key exchange protocols, - ; the current hybrid encryption scheme (RSA-OAEP/AES-256-GCM-SHA256) - ; will be replaced with double ratchet protocol and DH key exchange. -x509encoded = -encoded = -``` - -`hostname` can be IP address or domain name, as defined in RFC 1123, section 2.1. - -`port` is optional, the default TCP port for SMP protocol is 5223. - -`serverIdentity` is a required hash of the server certificate SPKI block (without line breaks, header and footer) used by the client to validate server certificate during transport handshake (see [Appendix A](#appendix-a)). - -Encryption keys are encoded using [X509][11] specification. - -Defining the approach to out-of-band message passing is out of scope of this protocol. +The approach to out-of-band message passing and their syntax should be defined in application-level protocols. ## Simplex queue @@ -133,6 +109,28 @@ Queue is defined by recipient ID `RID` and sender ID `SID`, unique for the serve The protocol uses different IDs for sender and recipient in order to provide an additional privacy by preventing the correlation of senders and recipients commands sent over the network - in case the encrypted transport is compromised, it would still be difficult to correlate senders and recipients without access to the queue records on the server. +## SMP queue URI + +The SMP queue URI should include queue hostname, an optional port, sender queue ID and server identity to establish secure connection with SMP server (see [Appendix A](#appendix-a) for SMP transport protocol). + +The [ABNF][8] syntax of the queue URI is: + +```abnf +queueURI = %s"smp://" smpServer "/" queueId +smpServer = serverIdentity "@" srvHost [":" port] +srvHost = ; RFC1123, RFC5891 +port = 1*DIGIT +serverIdentity = base64url +queueId = base64url +base64url = ; RFC4648, section 5 +``` + +`hostname` can be IP address or domain name, as defined in RFC 1123, section 2.1. + +`port` is optional, the default TCP port for SMP protocol is 5223. + +`serverIdentity` is a required hash of the server certificate SPKI block (without line breaks, header and footer) used by the client to validate server certificate during transport handshake (see [Appendix A](#appendix-a)) + ## SMP procedure The SMP procedure of creating a simplex queue on SMP server is explained using participants Alice (the recipient) who wants to receive messages from Bob (the sender). diff --git a/protocol/smp-uri-request.txt b/protocol/smp-uri-request.txt new file mode 100644 index 000000000..ffa3fd263 --- /dev/null +++ b/protocol/smp-uri-request.txt @@ -0,0 +1,16 @@ +Scheme name: smp + +Status: Provisional + +Applications/protocols that use this scheme name: +This scheme is used for URIs of message queues in SimpleX Messaging Protocol, +a client-server protocol for asynchronous distributed unidirectional +message transmission via persistent message queues. + +Contact: Evgeny Poberezkin + +Change controller: Evgeny Poberezkin + +References: +The syntax for message queue URIs in the latest version of SimpleX Messaging Protocol: +https://github.com/simplex-chat/simplexmq/blob/v5/protocol/simplex-messaging.md#smp-queue-uri From f3d79ebcda6ec2a9b575fc9527a7c37675af8d19 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 25 Nov 2021 17:24:00 +0000 Subject: [PATCH 04/20] update SMP protocol to include server signature and encryption keys --- protocol/simplex-messaging.md | 61 +++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 17 deletions(-) diff --git a/protocol/simplex-messaging.md b/protocol/simplex-messaging.md index ad08ccb89..8994ce449 100644 --- a/protocol/simplex-messaging.md +++ b/protocol/simplex-messaging.md @@ -111,12 +111,13 @@ The protocol uses different IDs for sender and recipient in order to provide an ## SMP queue URI -The SMP queue URI should include queue hostname, an optional port, sender queue ID and server identity to establish secure connection with SMP server (see [Appendix A](#appendix-a) for SMP transport protocol). +The SMP queue URIs MUST include server identity, queue hostname, an optional port, sender queue ID and the public key that the clients must use to verify responses. Server identity is used to establish secure connection protected from MITM attack with SMP server (see [Appendix A](#appendix-a) for SMP transport protocol). The [ABNF][8] syntax of the queue URI is: ```abnf -queueURI = %s"smp://" smpServer "/" queueId +queueURI = %s"smp://" smpServer "/" queueId "#" serverSignaturePublicKey +; serverSignaturePublicKey syntax is defined below smpServer = serverIdentity "@" srvHost [":" port] srvHost = ; RFC1123, RFC5891 port = 1*DIGIT @@ -281,7 +282,7 @@ Simplex Messaging Protocol: ## Cryptographic algorithms -Simplex messaging clients need to cryptographically sign commands for the following operations: +Simplex messaging clients and servers must cryptographically sign commands, responses and messages for the following operations: - With the recipient's key `RK` (server to verify): - create the queue (`NEW`) @@ -295,18 +296,22 @@ Simplex messaging clients need to cryptographically sign commands for the follow - send messages (`SEND`) - With the optional notifier's key: - subscribe to message notifications (`NSUB`) +- With the server's key (for recipient and sender to verify) + - queue IDs response (`IDS`) + - notifier queue ID response (`NID`) + - delivered messages (`MSG`) + - `OK` and `ERR` responses -To sign and verify commands, clients and servers MUST use RSA-PSS algorithm defined in [RFC3447][2]. +To sign/verify commands, messages and responses, clients and servers MUST use Ed25519 or Ed448 algorithm defined in [RFC8709][15]. -To optionally sign and verify messages, clients SHOULD use RSA-PSS algorithm. +To encrypt/decrypt message bodies delivered to the recipients, clients and servers MUST use x25519 or x448 algorithm defined in [RFC8709][15]. -To encrypt and decrypt messages, clients and servers SHOULD use RSA-OAEP algorithm defined in [RFC3447][2]. +Clients MUST encrypt message bodies sent via SMP servers - the protocol for this end-to-end encryption should be chosen by the clients using SMP protocol. The reasons to use these algorithms: -- They are supported by WebCrypto API. -- They are more widely supported than ECC algorithms. -- They are newer versions than RSA-PKCS1-v1_5 encryption and signature schemes. +- Faster operation that RSA algorithms. +- DH key exchange provides forward secrecy. Future versions of the protocol may allow different cryptographic algorithms. @@ -395,22 +400,37 @@ Sending any of the commands in this section (other than `create`, that is sent w This command is sent by the recipient to the SMP server to create a new queue. The syntax is: ```abnf -create = %s"NEW" SP recipientKey -recipientKey = signatureScheme ":" x509encoded ; the recipient's public key to verify commands for this queue -signatureScheme = %s"rsa" | %s"ed25519" | %s"ed448" ; "rsa" means deprecated RSA-PSS signature scheme, - ; it must not be used for the new queues. +create = %s"NEW" SP recipientSignaturePublicKey SP recipientDhPublicKey +recipientSignaturePublicKey = signatureKey +; the recipient's public key to verify commands for this queue +signatureKey = signatureScheme ":" x509encoded +signatureScheme = %s"rsa" | %s"ed25519" | %s"ed448" +; "rsa" means deprecated RSA-PSS signature scheme, +; it must not be used for the new queues. +recipientDhPublicKey = dhPublicKey +dhPublicKey = encryptionScheme ":" x509encoded +; the recipient's key for DH exchange to derive the secret +; that the server will use to encrypt delivered message bodies +encryptionScheme = %s"x25519" | %s"x448" + x509encoded = ``` -If the queue is created successfully, the server must send `queueIds` response with the recipient's and sender's queue IDs: +If the queue is created successfully, the server must send `queueIds` response with the recipient's and sender's queue IDs and public keys to sign all responses and messages and to encrypt delivered message bodies: ```abnf queueIds = %s"IDS" SP recipientId SP senderId + SP serverSignaturePublicKey SP serverDhPublicKey +serverSignaturePublicKey = signatureKey +; the server's public key to verify responses and messages for this queue +serverDhPublicKey = dhPublicKey +; the server's key for DH exchange to derive the secret +; that the server will use to encrypt delivered message bodies recipientId = encoded senderId = encoded ``` -This response should be sent with empty queue ID (the second part of the transmission). +This response should be sent with empty queue ID (the third part of the transmission). Once the queue is created, the recipient gets automatically subscribed to receive the messages from that queue, until the transport connection is closed. The `subscribe` command is needed only to start receiving the messages from the existing queue when the new transport connection is opened. @@ -457,7 +477,7 @@ notifierId = %s"NID" SP notifierId recipientId = encoded ``` -This response is sent with the recipient's queue ID (the second part of the transmission). +This response is sent with the recipient's queue ID (the third part of the transmission). To receive the message notifications, `subscribeNotifications` command ("NSUB") must be sent signed with the notifier's key. @@ -565,7 +585,9 @@ See its syntax in [Create queue command](#create-queue-command) The server must deliver messages to all subscribed simplex queues on the currently open transport connection. The syntax for the message delivery is: ```abnf -message = %s"MSG" SP msgId SP timestamp SP size SP msgBody SP +message = %s"MSG" SP encryptedMessage +encryptedMessage = +sentMessage = msgId SP timestamp SP size SP msgBody SP msgId = encoded timestamp = ``` @@ -576,6 +598,10 @@ timestamp = `msgBody` - see syntax in [Send message](#send-message) +When server delivers the messages to the recipient, message body should be encrypted with the secret derived from DH exchange using the keys passed during the queue creation and returned with `queueIds` response. + +This is done to prevent the possibility of correlation of incoming and outgoing traffic of SMP server inside transport protocol. + #### Notifier queue ID response Server must respond with this message when queue notifications are enabled. @@ -698,3 +724,4 @@ The communication party (client or server) that has the lower protocol version s [12]: https://tools.ietf.org/html/rfc7714 [13]: https://datatracker.ietf.org/doc/html/rfc8446 [14]: https://datatracker.ietf.org/doc/html/rfc5929#section-3 +[15]: https://www.rfc-editor.org/rfc/rfc8709.html From ab875198ed27b26a38d5cc741200e799e73e4072 Mon Sep 17 00:00:00 2001 From: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com> Date: Sat, 27 Nov 2021 02:31:15 +1000 Subject: [PATCH 05/20] SMP protocol typo --- protocol/simplex-messaging.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protocol/simplex-messaging.md b/protocol/simplex-messaging.md index 8994ce449..d90b3353b 100644 --- a/protocol/simplex-messaging.md +++ b/protocol/simplex-messaging.md @@ -310,7 +310,7 @@ Clients MUST encrypt message bodies sent via SMP servers - the protocol for this The reasons to use these algorithms: -- Faster operation that RSA algorithms. +- Faster operation than RSA algorithms. - DH key exchange provides forward secrecy. Future versions of the protocol may allow different cryptographic algorithms. From 99b374989024f7dd6eefca78ed0388afaff3cc41 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 28 Nov 2021 07:08:47 +0000 Subject: [PATCH 06/20] simplify RSA private key types (#207) * simplify RSA private key types * remove updateSignKey method --- apps/smp-server/Main.hs | 10 +- protocol/agent-protocol.md | 4 +- src/Simplex/Messaging/Agent.hs | 26 ++-- src/Simplex/Messaging/Agent/Protocol.hs | 4 +- src/Simplex/Messaging/Agent/Store.hs | 1 - src/Simplex/Messaging/Agent/Store/SQLite.hs | 12 -- src/Simplex/Messaging/Client.hs | 4 +- src/Simplex/Messaging/Crypto.hs | 130 ++++---------------- src/Simplex/Messaging/Protocol.hs | 6 +- src/Simplex/Messaging/Server/Env/STM.hs | 4 +- src/Simplex/Messaging/Transport.hs | 4 +- tests/AgentTests/SQLiteTests.hs | 34 +++-- tests/ServerTests.hs | 4 +- 13 files changed, 75 insertions(+), 168 deletions(-) diff --git a/apps/smp-server/Main.hs b/apps/smp-server/Main.hs index b71bed5d4..ba8cfe702 100644 --- a/apps/smp-server/Main.hs +++ b/apps/smp-server/Main.hs @@ -101,7 +101,7 @@ getConfig opts = do storeLog <- liftIO $ openStoreLog opts ini pure $ makeConfig ini pk storeLog -makeConfig :: IniOpts -> C.FullPrivateKey -> Maybe (StoreLog 'ReadMode) -> ServerConfig +makeConfig :: IniOpts -> C.PrivateKey -> Maybe (StoreLog 'ReadMode) -> ServerConfig makeConfig IniOpts {serverPort, blockSize, enableWebsockets} pk storeLog = let transports = (serverPort, transport @TCP) : [("80", transport @WS) | enableWebsockets] in serverConfig {serverPrivateKey = pk, storeLog, blockSize, transports} @@ -200,11 +200,11 @@ createIni ServerOpts {enableStoreLog} = do enableWebsockets = True } -readKey :: IniOpts -> ExceptT String IO C.FullPrivateKey +readKey :: IniOpts -> ExceptT String IO C.PrivateKey readKey IniOpts {serverKeyFile} = do fileExists serverKeyFile liftIO (S.readKeyFile serverKeyFile) >>= \case - [S.Unprotected (PrivKeyRSA pk)] -> pure $ C.FullPrivateKey pk + [S.Unprotected (PrivKeyRSA pk)] -> pure $ C.PrivateKey pk [_] -> err "not RSA key" [] -> err "invalid key file format" _ -> err "more than one key" @@ -212,7 +212,7 @@ readKey IniOpts {serverKeyFile} = do err :: String -> ExceptT String IO b err e = throwE $ e <> ": " <> serverKeyFile -createKey :: IniOpts -> IO C.FullPrivateKey +createKey :: IniOpts -> IO C.PrivateKey createKey IniOpts {serverKeyFile} = do (_, pk) <- C.generateKeyPair newKeySize S.writeKeyFile S.TraditionalFormat serverKeyFile [PrivKeyRSA $ C.rsaPrivateKey pk] @@ -233,7 +233,7 @@ confirm msg = do ok <- getLine when (map toLower ok /= "y") exitFailure -serverKeyHash :: C.FullPrivateKey -> B.ByteString +serverKeyHash :: C.PrivateKey -> B.ByteString serverKeyHash = encode . C.unKeyHash . C.publicKeyHash . C.publicKey' openStoreLog :: ServerOpts -> IniOpts -> IO (Maybe (StoreLog 'ReadMode)) diff --git a/protocol/agent-protocol.md b/protocol/agent-protocol.md index 417a75853..41c1297b4 100644 --- a/protocol/agent-protocol.md +++ b/protocol/agent-protocol.md @@ -40,9 +40,7 @@ It provides: SMP agent protocol provides no encryption or security on the client side - it is assumed that the agent is executed in the trusted and secure environment, in one of three ways: - via TCP network using secure connection. - via local port (when the agent runs on the same device as a separate process). -- via agent library, when the agent logic is included directly into the client application. - -The last option is the most secure, as it reduces the number of attack vectors in comparison with other options. [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat) uses this approach. +- via agent library, when the agent logic is included directly into the client application - [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat) uses this approach. ## SMP agent diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index b539cf501..7b48bcc04 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -298,30 +298,27 @@ subscribeConnection' c connId = SomeConn _ (DuplexConnection _ rq sq) -> do resumeMsgDelivery c connId sq case status (sq :: SndQueue) of - Confirmed -> withVerifyKey sq $ \verifyKey -> do + Confirmed -> do conf <- withStore (`getAcceptedConfirmation` connId) secureQueue c rq $ senderKey (conf :: AcceptedConfirmation) withStore $ \st -> setRcvQueueStatus st rq Secured - activateSecuredQueue rq sq verifyKey - Secured -> withVerifyKey sq $ activateSecuredQueue rq sq + activateSecuredQueue rq sq + Secured -> activateSecuredQueue rq sq Active -> subscribeQueue c rq connId _ -> throwError $ INTERNAL "unexpected queue status" SomeConn _ (SndConnection _ sq) -> do resumeMsgDelivery c connId sq case status (sq :: SndQueue) of - Confirmed -> withVerifyKey sq $ \verifyKey -> - activateQueueJoining c connId sq verifyKey =<< resumeInterval + Confirmed -> activateQueueJoining c connId sq (verifyKey sq) =<< resumeInterval Active -> throwError $ CONN SIMPLEX _ -> throwError $ INTERNAL "unexpected queue status" SomeConn _ (RcvConnection _ rq) -> subscribeQueue c rq connId where - withVerifyKey :: SndQueue -> (C.PublicKey -> m ()) -> m () - withVerifyKey sq action = - let err = throwError $ INTERNAL "missing signing key public counterpart" - in maybe err action . C.publicKey $ signKey sq - activateSecuredQueue :: RcvQueue -> SndQueue -> C.PublicKey -> m () - activateSecuredQueue rq sq verifyKey = do - activateQueueInitiating c connId sq verifyKey =<< resumeInterval + verifyKey :: SndQueue -> C.PublicKey + verifyKey = C.publicKey' . signKey + activateSecuredQueue :: RcvQueue -> SndQueue -> m () + activateSecuredQueue rq sq = do + activateQueueInitiating c connId sq (verifyKey sq) =<< resumeInterval subscribeQueue c rq connId resumeInterval :: m RetryInterval resumeInterval = do @@ -600,12 +597,7 @@ activateQueue c connId sq verifyKey retryInterval afterActivation = sendHello c sq verifyKey retryInterval withStore $ \st -> setSndQueueStatus st sq Active removeActivation c connId - removeVerificationKey afterActivation - removeVerificationKey :: m () - removeVerificationKey = - let safeSignKey = C.removePublicKey $ signKey sq - in withStore $ \st -> updateSignKey st sq safeSignKey notifyConnected :: AgentMonad m => AgentClient -> ConnId -> m () notifyConnected c connId = atomically $ writeTBQueue (subQ c) ("", connId, CON) diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index 44e42cdd4..0614a2df8 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -339,10 +339,10 @@ data SMPQueueInfo = SMPQueueInfo SMPServer SMP.SenderId EncryptionKey type EncryptionKey = C.PublicKey -- | Private key used to E2E decrypt SMP messages. -type DecryptionKey = C.SafePrivateKey +type DecryptionKey = C.PrivateKey -- | Private key used to sign SMP commands -type SignatureKey = C.APrivateKey +type SignatureKey = C.PrivateKey -- | Public key used by SMP server to authorize (verify) SMP commands. type VerificationKey = C.PublicKey diff --git a/src/Simplex/Messaging/Agent/Store.hs b/src/Simplex/Messaging/Agent/Store.hs index 7ef0e7f1e..f6bdec954 100644 --- a/src/Simplex/Messaging/Agent/Store.hs +++ b/src/Simplex/Messaging/Agent/Store.hs @@ -43,7 +43,6 @@ class Monad m => MonadAgentStore s m where setRcvQueueStatus :: s -> RcvQueue -> QueueStatus -> m () setRcvQueueActive :: s -> RcvQueue -> VerificationKey -> m () setSndQueueStatus :: s -> SndQueue -> QueueStatus -> m () - updateSignKey :: s -> SndQueue -> SignatureKey -> m () -- Confirmations createConfirmation :: s -> TVar ChaChaDRG -> NewConfirmation -> m ConfirmationId diff --git a/src/Simplex/Messaging/Agent/Store/SQLite.hs b/src/Simplex/Messaging/Agent/Store/SQLite.hs index ec05920d6..177dd2950 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite.hs @@ -279,18 +279,6 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto |] [":status" := status, ":host" := host, ":port" := serializePort_ port, ":snd_id" := sndId] - updateSignKey :: SQLiteStore -> SndQueue -> SignatureKey -> m () - updateSignKey st SndQueue {sndId, server = SMPServer {host, port}} signatureKey = - liftIO . withTransaction st $ \db -> - DB.executeNamed - db - [sql| - UPDATE snd_queues - SET sign_key = :sign_key - WHERE host = :host AND port = :port AND snd_id = :snd_id; - |] - [":sign_key" := signatureKey, ":host" := host, ":port" := serializePort_ port, ":snd_id" := sndId] - createConfirmation :: SQLiteStore -> TVar ChaChaDRG -> NewConfirmation -> m ConfirmationId createConfirmation st gVar NewConfirmation {connId, senderKey, senderConnInfo} = liftIOEither . withTransaction st $ \db -> diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index f39dc2e05..7c845d809 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -333,14 +333,14 @@ suspendSMPQueue = okSMPCommand $ Cmd SRecipient OFF deleteSMPQueue :: SMPClient -> RecipientPrivateKey -> QueueId -> ExceptT SMPClientError IO () deleteSMPQueue = okSMPCommand $ Cmd SRecipient DEL -okSMPCommand :: Cmd -> SMPClient -> C.SafePrivateKey -> QueueId -> ExceptT SMPClientError IO () +okSMPCommand :: Cmd -> SMPClient -> C.PrivateKey -> QueueId -> ExceptT SMPClientError IO () okSMPCommand cmd c pKey qId = sendSMPCommand c (Just pKey) qId cmd >>= \case Cmd _ OK -> return () _ -> throwE SMPUnexpectedResponse -- | Send any SMP command ('Cmd' type). -sendSMPCommand :: SMPClient -> Maybe C.SafePrivateKey -> QueueId -> Cmd -> ExceptT SMPClientError IO Cmd +sendSMPCommand :: SMPClient -> Maybe C.PrivateKey -> QueueId -> Cmd -> ExceptT SMPClientError IO Cmd sendSMPCommand SMPClient {sndQ, sentCommands, clientCorrId, tcpTimeout} pKey qId cmd = do corrId <- lift_ getNextCorrId t <- signTransmission $ serializeTransmission (corrId, qId, cmd) diff --git a/src/Simplex/Messaging/Crypto.hs b/src/Simplex/Messaging/Crypto.hs index ccd8f5f90..a92c75ffd 100644 --- a/src/Simplex/Messaging/Crypto.hs +++ b/src/Simplex/Messaging/Crypto.hs @@ -20,20 +20,14 @@ -- . module Simplex.Messaging.Crypto ( -- * RSA keys - PrivateKey (rsaPrivateKey, publicKey), - SafePrivateKey (..), -- constructor is not exported - FullPrivateKey (..), - APrivateKey (..), + PrivateKey (..), PublicKey (..), - SafeKeyPair, - FullKeyPair, + KeyPair, KeyHash (..), generateKeyPair, publicKey', publicKeySize, validKeySize, - safePrivateKey, - removePublicKey, -- * E2E hybrid encryption scheme encrypt, @@ -114,86 +108,25 @@ import Simplex.Messaging.Util (liftEitherError, (<$?>)) -- | A newtype of 'Crypto.PubKey.RSA.PublicKey'. newtype PublicKey = PublicKey {rsaPublicKey :: R.PublicKey} deriving (Eq, Show) --- | A newtype of 'Crypto.PubKey.RSA.PrivateKey', with PublicKey removed. --- --- It is not possible to recover PublicKey from SafePrivateKey. --- The constructor of this type is not exported. -newtype SafePrivateKey = SafePrivateKey {unPrivateKey :: R.PrivateKey} deriving (Eq, Show) - --- | A newtype of 'Crypto.PubKey.RSA.PrivateKey' (with PublicKey inside). -newtype FullPrivateKey = FullPrivateKey {unPrivateKey :: R.PrivateKey} deriving (Eq, Show) - -- | A newtype of 'Crypto.PubKey.RSA.PrivateKey' (PublicKey may be inside). -newtype APrivateKey = APrivateKey {unPrivateKey :: R.PrivateKey} deriving (Eq, Show) +newtype PrivateKey = PrivateKey {rsaPrivateKey :: R.PrivateKey} deriving (Eq, Show) --- | Type-class used for both private key types: SafePrivateKey and FullPrivateKey. -class PrivateKey k where - -- unwraps 'Crypto.PubKey.RSA.PrivateKey' - rsaPrivateKey :: k -> R.PrivateKey - - -- equivalent to data type constructor, not exported - _privateKey :: R.PrivateKey -> k - - -- smart constructor removing public key from SafePrivateKey but keeping it in FullPrivateKey - mkPrivateKey :: R.PrivateKey -> k - - -- extracts public key from private key - publicKey :: k -> Maybe PublicKey - --- | Remove public key exponent from APrivateKey. -removePublicKey :: APrivateKey -> APrivateKey -removePublicKey (APrivateKey R.PrivateKey {private_pub = k, private_d}) = - APrivateKey $ unPrivateKey (safePrivateKey (R.public_size k, R.public_n k, private_d) :: SafePrivateKey) - -instance PrivateKey SafePrivateKey where - rsaPrivateKey = unPrivateKey - _privateKey = SafePrivateKey - mkPrivateKey R.PrivateKey {private_pub = k, private_d} = - safePrivateKey (R.public_size k, R.public_n k, private_d) - publicKey _ = Nothing - -instance PrivateKey FullPrivateKey where - rsaPrivateKey = unPrivateKey - _privateKey = FullPrivateKey - mkPrivateKey = FullPrivateKey - publicKey = Just . PublicKey . R.private_pub . rsaPrivateKey - -instance PrivateKey APrivateKey where - rsaPrivateKey = unPrivateKey - _privateKey = APrivateKey - mkPrivateKey = APrivateKey - publicKey pk = - let k = R.private_pub $ rsaPrivateKey pk - in if R.public_e k == 0 - then Nothing - else Just $ PublicKey k - -instance IsString FullPrivateKey where +instance IsString PrivateKey where fromString = parseString $ decode >=> decodePrivKey instance IsString PublicKey where fromString = parseString $ decode >=> decodePubKey -instance ToField SafePrivateKey where toField = toField . encodePrivKey - -instance ToField APrivateKey where toField = toField . encodePrivKey +instance ToField PrivateKey where toField = toField . encodePrivKey instance ToField PublicKey where toField = toField . encodePubKey -instance FromField SafePrivateKey where fromField = blobFieldParser binaryPrivKeyP - -instance FromField APrivateKey where fromField = blobFieldParser binaryPrivKeyP +instance FromField PrivateKey where fromField = blobFieldParser binaryPrivKeyP instance FromField PublicKey where fromField = blobFieldParser binaryPubKeyP -- | Tuple of RSA 'PublicKey' and 'PrivateKey'. -type KeyPair k = (PublicKey, k) - --- | Tuple of RSA 'PublicKey' and 'SafePrivateKey'. -type SafeKeyPair = (PublicKey, SafePrivateKey) - --- | Tuple of RSA 'PublicKey' and 'FullPrivateKey'. -type FullKeyPair = (PublicKey, FullPrivateKey) +type KeyPair = (PublicKey, PrivateKey) -- | RSA signature newtype. newtype Signature = Signature {unSignature :: ByteString} deriving (Eq, Show) @@ -230,8 +163,8 @@ aesKeySize = 256 `div` 8 authTagSize :: Int authTagSize = 128 `div` 8 --- | Generate RSA key pair with either SafePrivateKey or FullPrivateKey. -generateKeyPair :: PrivateKey k => Int -> IO (KeyPair k) +-- | Generate RSA key pair. +generateKeyPair :: Int -> IO KeyPair generateKeyPair size = loop where publicExponent = findPrimeFrom . (+ 3) <$> generateMax pubExpRange @@ -241,12 +174,12 @@ generateKeyPair size = loop d = R.private_d pk if d * d < n then loop - else pure (PublicKey k, mkPrivateKey pk) + else pure (PublicKey k, PrivateKey pk) -privateKeySize :: PrivateKey k => k -> Int +privateKeySize :: PrivateKey -> Int privateKeySize = R.public_size . R.private_pub . rsaPrivateKey -publicKey' :: FullPrivateKey -> PublicKey +publicKey' :: PrivateKey -> PublicKey publicKey' = PublicKey . R.private_pub . rsaPrivateKey publicKeySize :: PublicKey -> Int @@ -331,7 +264,7 @@ encrypt k paddedSize msg = do -- | E2E decrypt SMP agent messages. -- -- https://github.com/simplex-chat/simplexmq/blob/master/rfcs/2021-01-26-crypto.md#e2e-encryption -decrypt :: PrivateKey k => k -> ByteString -> ExceptT CryptoError IO ByteString +decrypt :: PrivateKey -> ByteString -> ExceptT CryptoError IO ByteString decrypt pk msg'' = do let (encHeader, msg') = B.splitAt (privateKeySize pk) msg'' header <- decryptOAEP pk encHeader @@ -410,7 +343,7 @@ encryptOAEP (PublicKey k) aesKey = -- | RSA OAEP decryption. -- -- Used as part of hybrid E2E encryption scheme and for SMP transport handshake. -decryptOAEP :: PrivateKey k => k -> ByteString -> ExceptT CryptoError IO ByteString +decryptOAEP :: PrivateKey -> ByteString -> ExceptT CryptoError IO ByteString decryptOAEP pk encKey = liftEitherError RSADecryptError $ OAEP.decryptSafer oaepParams (rsaPrivateKey pk) encKey @@ -421,7 +354,7 @@ pssParams = PSS.defaultPSSParams SHA256 -- | RSA PSS message signing. -- -- Used by SMP clients to sign SMP commands and by SMP agents to sign messages. -sign :: PrivateKey k => k -> ByteString -> ExceptT CryptoError IO Signature +sign :: PrivateKey -> ByteString -> ExceptT CryptoError IO Signature sign pk msg = ExceptT $ bimap RSASignError Signature <$> PSS.signSafer pssParams (rsaPrivateKey pk) msg -- | RSA PSS signature verification. @@ -439,7 +372,7 @@ serializePubKey = ("rsa:" <>) . encode . encodePubKey -- | Base-64 PKCS8 encoding of PSA private key. -- -- Not used as part of SMP protocols. -serializePrivKey :: PrivateKey k => k -> ByteString +serializePrivKey :: PrivateKey -> ByteString serializePrivKey = ("rsa:" <>) . encode . encodePrivKey -- Base-64 X509 RSA public key parser. @@ -451,40 +384,19 @@ binaryPubKeyP :: Parser PublicKey binaryPubKeyP = decodePubKey <$?> A.takeByteString -- Base-64 PKCS8 RSA private key parser. -privKeyP :: PrivateKey k => Parser k +privKeyP :: Parser PrivateKey privKeyP = decodePrivKey <$?> ("rsa:" *> base64P) -- Binary PKCS8 RSA private key parser. -binaryPrivKeyP :: PrivateKey k => Parser k +binaryPrivKeyP :: Parser PrivateKey binaryPrivKeyP = decodePrivKey <$?> A.takeByteString --- | Construct 'SafePrivateKey' from three numbers - used internally and in the tests. -safePrivateKey :: (Int, Integer, Integer) -> SafePrivateKey -safePrivateKey = SafePrivateKey . safeRsaPrivateKey - -safeRsaPrivateKey :: (Int, Integer, Integer) -> R.PrivateKey -safeRsaPrivateKey (size, n, d) = - R.PrivateKey - { private_pub = - R.PublicKey - { public_size = size, - public_n = n, - public_e = 0 - }, - private_d = d, - private_p = 0, - private_q = 0, - private_dP = 0, - private_dQ = 0, - private_qinv = 0 - } - -- Binary X509 encoding of 'PublicKey'. encodePubKey :: PublicKey -> ByteString encodePubKey = encodeKey . PubKeyRSA . rsaPublicKey -- Binary PKCS8 encoding of 'PrivateKey'. -encodePrivKey :: PrivateKey k => k -> ByteString +encodePrivKey :: PrivateKey -> ByteString encodePrivKey = encodeKey . PrivKeyRSA . rsaPrivateKey encodeKey :: ASN1Object a => a -> ByteString @@ -498,10 +410,10 @@ decodePubKey = r -> keyError r -- Decoding of binary PKCS8 'PrivateKey'. -decodePrivKey :: PrivateKey k => ByteString -> Either String k +decodePrivKey :: ByteString -> Either String PrivateKey decodePrivKey = decodeKey >=> \case - (PrivKeyRSA pk, []) -> Right $ mkPrivateKey pk + (PrivKeyRSA pk, []) -> Right $ PrivateKey pk r -> keyError r decodeKey :: ASN1Object a => ByteString -> Either String (a, [ASN1]) diff --git a/src/Simplex/Messaging/Protocol.hs b/src/Simplex/Messaging/Protocol.hs index 02056acc4..d80f25ed5 100644 --- a/src/Simplex/Messaging/Protocol.hs +++ b/src/Simplex/Messaging/Protocol.hs @@ -177,7 +177,7 @@ instance IsString CorrId where -- | Recipient's private key used by the recipient to authorize (sign) SMP commands. -- -- Only used by SMP agent, kept here so its definition is close to respective public key. -type RecipientPrivateKey = C.SafePrivateKey +type RecipientPrivateKey = C.PrivateKey -- | Recipient's public key used by SMP server to verify authorization of SMP commands. type RecipientPublicKey = C.PublicKey @@ -185,13 +185,13 @@ type RecipientPublicKey = C.PublicKey -- | Sender's private key used by the recipient to authorize (sign) SMP commands. -- -- Only used by SMP agent, kept here so its definition is close to respective public key. -type SenderPrivateKey = C.SafePrivateKey +type SenderPrivateKey = C.PrivateKey -- | Sender's public key used by SMP server to verify authorization of SMP commands. type SenderPublicKey = C.PublicKey -- | Private key used by push notifications server to authorize (sign) LSTN command. -type NotifierPrivateKey = C.SafePrivateKey +type NotifierPrivateKey = C.PrivateKey -- | Public key used by SMP server to verify authorization of LSTN command sent by push notifications server. type NotifierPublicKey = C.PublicKey diff --git a/src/Simplex/Messaging/Server/Env/STM.hs b/src/Simplex/Messaging/Server/Env/STM.hs index 5a0ebacea..83f2f2633 100644 --- a/src/Simplex/Messaging/Server/Env/STM.hs +++ b/src/Simplex/Messaging/Server/Env/STM.hs @@ -30,7 +30,7 @@ data ServerConfig = ServerConfig msgIdBytes :: Int, storeLog :: Maybe (StoreLog 'ReadMode), blockSize :: Int, - serverPrivateKey :: C.FullPrivateKey + serverPrivateKey :: C.PrivateKey -- serverId :: ByteString } @@ -40,7 +40,7 @@ data Env = Env queueStore :: QueueStore, msgStore :: STMMsgStore, idsDrg :: TVar ChaChaDRG, - serverKeyPair :: C.FullKeyPair, + serverKeyPair :: C.KeyPair, storeLog :: Maybe (StoreLog 'WriteMode) } diff --git a/src/Simplex/Messaging/Transport.hs b/src/Simplex/Messaging/Transport.hs index 06382563c..6c15576b6 100644 --- a/src/Simplex/Messaging/Transport.hs +++ b/src/Simplex/Messaging/Transport.hs @@ -63,7 +63,7 @@ import Data.ByteArray (xor) import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.Functor (($>)) -import Data.Maybe(fromMaybe) +import Data.Maybe (fromMaybe) import Data.Set (Set) import qualified Data.Set as S import Data.String @@ -345,7 +345,7 @@ makeNextIV SessionKey {baseIV, counter} = atomically $ do -- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#appendix-a -- -- The numbers in function names refer to the steps in the document. -serverHandshake :: forall c. Transport c => c -> Int -> C.FullKeyPair -> ExceptT TransportError IO (THandle c) +serverHandshake :: forall c. Transport c => c -> Int -> C.KeyPair -> ExceptT TransportError IO (THandle c) serverHandshake c srvBlockSize (k, pk) = do checkValidBlockSize srvBlockSize liftIO sendHeaderAndPublicKey_1 diff --git a/tests/AgentTests/SQLiteTests.hs b/tests/AgentTests/SQLiteTests.hs index 26d652ad9..83333fea5 100644 --- a/tests/AgentTests/SQLiteTests.hs +++ b/tests/AgentTests/SQLiteTests.hs @@ -149,14 +149,32 @@ testForeignKeysEnabled = cData1 :: ConnData cData1 = ConnData {connId = "conn1"} +testPrivateKey :: C.PrivateKey +testPrivateKey = + C.PrivateKey + R.PrivateKey + { private_pub = + R.PublicKey + { public_size = 1, + public_n = 2, + public_e = 0 + }, + private_d = 3, + private_p = 0, + private_q = 0, + private_dP = 0, + private_dQ = 0, + private_qinv = 0 + } + rcvQueue1 :: RcvQueue rcvQueue1 = RcvQueue { server = SMPServer "smp.simplex.im" (Just "5223") testKeyHash, rcvId = "1234", - rcvPrivateKey = C.safePrivateKey (1, 2, 3), + rcvPrivateKey = testPrivateKey, sndId = Just "2345", - decryptKey = C.safePrivateKey (1, 2, 3), + decryptKey = testPrivateKey, verifyKey = Nothing, status = New } @@ -166,9 +184,9 @@ sndQueue1 = SndQueue { server = SMPServer "smp.simplex.im" (Just "5223") testKeyHash, sndId = "3456", - sndPrivateKey = C.safePrivateKey (1, 2, 3), + sndPrivateKey = testPrivateKey, encryptKey = C.PublicKey $ R.PublicKey 1 2 3, - signKey = C.APrivateKey $ C.unPrivateKey (C.safePrivateKey (1, 2, 3) :: C.SafePrivateKey), + signKey = testPrivateKey, status = New } @@ -306,9 +324,9 @@ testUpgradeRcvConnToDuplex = SndQueue { server = SMPServer "smp.simplex.im" (Just "5223") testKeyHash, sndId = "2345", - sndPrivateKey = C.safePrivateKey (1, 2, 3), + sndPrivateKey = testPrivateKey, encryptKey = C.PublicKey $ R.PublicKey 1 2 3, - signKey = C.APrivateKey $ C.unPrivateKey (C.safePrivateKey (1, 2, 3) :: C.SafePrivateKey), + signKey = testPrivateKey, status = New } upgradeRcvConnToDuplex store "conn1" anotherSndQueue @@ -326,9 +344,9 @@ testUpgradeSndConnToDuplex = RcvQueue { server = SMPServer "smp.simplex.im" (Just "5223") testKeyHash, rcvId = "3456", - rcvPrivateKey = C.safePrivateKey (1, 2, 3), + rcvPrivateKey = testPrivateKey, sndId = Just "4567", - decryptKey = C.safePrivateKey (1, 2, 3), + decryptKey = testPrivateKey, verifyKey = Nothing, status = New } diff --git a/tests/ServerTests.hs b/tests/ServerTests.hs index 3d328a4af..7c12b9e18 100644 --- a/tests/ServerTests.hs +++ b/tests/ServerTests.hs @@ -47,7 +47,7 @@ pattern Resp corrId queueId command <- ("", (corrId, queueId, Right (Cmd SBroker sendRecv :: Transport c => THandle c -> (ByteString, ByteString, ByteString, ByteString) -> IO SignedTransmissionOrError sendRecv h (sgn, corrId, qId, cmd) = tPutRaw h (sgn, corrId, encode qId, cmd) >> tGet fromServer h -signSendRecv :: Transport c => THandle c -> C.SafePrivateKey -> (ByteString, ByteString, ByteString) -> IO SignedTransmissionOrError +signSendRecv :: Transport c => THandle c -> C.PrivateKey -> (ByteString, ByteString, ByteString) -> IO SignedTransmissionOrError signSendRecv h pk (corrId, qId, cmd) = do let t = B.intercalate " " [corrId, encode qId, cmd] Right sig <- runExceptT $ C.sign pk t @@ -332,7 +332,7 @@ testWithStoreLog at@(ATransport t) = Right l -> pure l Left (_ :: SomeException) -> logSize -createAndSecureQueue :: Transport c => THandle c -> SenderPublicKey -> IO (SenderId, RecipientId, C.SafePrivateKey) +createAndSecureQueue :: Transport c => THandle c -> SenderPublicKey -> IO (SenderId, RecipientId, C.PrivateKey) createAndSecureQueue h sPub = do (rPub, rKey) <- C.generateKeyPair rsaKeySize Resp "abcd" "" (IDS rId sId) <- signSendRecv h rKey ("abcd", "", "NEW " <> C.serializePubKey rPub) From e1002d5ac075e125d50de8410e0ff474c44eda23 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 28 Nov 2021 11:44:22 +0000 Subject: [PATCH 07/20] GADTs for cryptographic keys (#208) * GADTs for cryptographic keys * update tests (signature timing tests still fail) * fix signature verification timing tests * configurable algorithm to sign commands to SMP queues (Ed448 by default) * add dummy Ed keys, add timing tests for Ed keys * re-enable Connection subscriptions tests --- apps/smp-server/Main.hs | 16 +- src/Simplex/Messaging/Agent.hs | 27 +- src/Simplex/Messaging/Agent/Client.hs | 38 +- src/Simplex/Messaging/Agent/Env/SQLite.hs | 3 + src/Simplex/Messaging/Agent/Protocol.hs | 34 +- src/Simplex/Messaging/Agent/Store.hs | 11 +- src/Simplex/Messaging/Agent/Store/SQLite.hs | 3 +- src/Simplex/Messaging/Client.hs | 16 +- src/Simplex/Messaging/Crypto.hs | 727 +++++++++++++++++--- src/Simplex/Messaging/Protocol.hs | 72 +- src/Simplex/Messaging/Server.hs | 78 ++- src/Simplex/Messaging/Server/Env/STM.hs | 6 +- src/Simplex/Messaging/Server/StoreLog.hs | 20 +- src/Simplex/Messaging/Transport.hs | 15 +- tests/AgentTests/SQLiteTests.hs | 34 +- tests/SMPClient.hs | 17 +- tests/ServerTests.hs | 159 +++-- 17 files changed, 938 insertions(+), 338 deletions(-) diff --git a/apps/smp-server/Main.hs b/apps/smp-server/Main.hs index ba8cfe702..75c5080ec 100644 --- a/apps/smp-server/Main.hs +++ b/apps/smp-server/Main.hs @@ -101,7 +101,7 @@ getConfig opts = do storeLog <- liftIO $ openStoreLog opts ini pure $ makeConfig ini pk storeLog -makeConfig :: IniOpts -> C.PrivateKey -> Maybe (StoreLog 'ReadMode) -> ServerConfig +makeConfig :: IniOpts -> C.PrivateKey 'C.RSA -> Maybe (StoreLog 'ReadMode) -> ServerConfig makeConfig IniOpts {serverPort, blockSize, enableWebsockets} pk storeLog = let transports = (serverPort, transport @TCP) : [("80", transport @WS) | enableWebsockets] in serverConfig {serverPrivateKey = pk, storeLog, blockSize, transports} @@ -200,11 +200,11 @@ createIni ServerOpts {enableStoreLog} = do enableWebsockets = True } -readKey :: IniOpts -> ExceptT String IO C.PrivateKey +readKey :: IniOpts -> ExceptT String IO (C.PrivateKey 'C.RSA) readKey IniOpts {serverKeyFile} = do fileExists serverKeyFile liftIO (S.readKeyFile serverKeyFile) >>= \case - [S.Unprotected (PrivKeyRSA pk)] -> pure $ C.PrivateKey pk + [S.Unprotected (PrivKeyRSA pk)] -> pure $ C.PrivateKeyRSA pk [_] -> err "not RSA key" [] -> err "invalid key file format" _ -> err "more than one key" @@ -212,10 +212,10 @@ readKey IniOpts {serverKeyFile} = do err :: String -> ExceptT String IO b err e = throwE $ e <> ": " <> serverKeyFile -createKey :: IniOpts -> IO C.PrivateKey +createKey :: IniOpts -> IO (C.PrivateKey 'C.RSA) createKey IniOpts {serverKeyFile} = do - (_, pk) <- C.generateKeyPair newKeySize - S.writeKeyFile S.TraditionalFormat serverKeyFile [PrivKeyRSA $ C.rsaPrivateKey pk] + (_, pk) <- C.generateKeyPair' newKeySize C.SRSA + S.writeKeyFile S.TraditionalFormat serverKeyFile [C.privateToX509 pk] pure pk fileExists :: FilePath -> ExceptT String IO () @@ -233,8 +233,8 @@ confirm msg = do ok <- getLine when (map toLower ok /= "y") exitFailure -serverKeyHash :: C.PrivateKey -> B.ByteString -serverKeyHash = encode . C.unKeyHash . C.publicKeyHash . C.publicKey' +serverKeyHash :: C.PrivateKey 'C.RSA -> B.ByteString +serverKeyHash = encode . C.unKeyHash . C.publicKeyHash . C.publicKey openStoreLog :: ServerOpts -> IniOpts -> IO (Maybe (StoreLog 'ReadMode)) openStoreLog ServerOpts {enableStoreLog = l} IniOpts {enableStoreLog = l', storeLogFile = f} diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 7b48bcc04..bdc76278e 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -264,7 +264,7 @@ joinConn c connId qInfo cInfo = do activateQueueJoining c connId' sq verifyKey $ retryInterval cfg pure connId' -activateQueueJoining :: forall m. AgentMonad m => AgentClient -> ConnId -> SndQueue -> VerificationKey -> RetryInterval -> m () +activateQueueJoining :: forall m. AgentMonad m => AgentClient -> ConnId -> SndQueue -> C.APublicVerifyKey -> RetryInterval -> m () activateQueueJoining c connId sq verifyKey retryInterval = activateQueue c connId sq verifyKey retryInterval createReplyQueue where @@ -314,8 +314,8 @@ subscribeConnection' c connId = _ -> throwError $ INTERNAL "unexpected queue status" SomeConn _ (RcvConnection _ rq) -> subscribeQueue c rq connId where - verifyKey :: SndQueue -> C.PublicKey - verifyKey = C.publicKey' . signKey + verifyKey :: SndQueue -> C.APublicVerifyKey + verifyKey = C.publicKey . signKey activateSecuredQueue :: RcvQueue -> SndQueue -> m () activateSecuredQueue rq sq = do activateQueueInitiating c connId sq (verifyKey sq) =<< resumeInterval @@ -582,11 +582,11 @@ confirmQueue c sq senderKey cInfo = do sendConfirmation c sq senderKey cInfo withStore $ \st -> setSndQueueStatus st sq Confirmed -activateQueueInitiating :: AgentMonad m => AgentClient -> ConnId -> SndQueue -> VerificationKey -> RetryInterval -> m () +activateQueueInitiating :: AgentMonad m => AgentClient -> ConnId -> SndQueue -> C.APublicVerifyKey -> RetryInterval -> m () activateQueueInitiating c connId sq verifyKey retryInterval = activateQueue c connId sq verifyKey retryInterval $ notifyConnected c connId -activateQueue :: forall m. AgentMonad m => AgentClient -> ConnId -> SndQueue -> VerificationKey -> RetryInterval -> m () -> m () +activateQueue :: forall m. AgentMonad m => AgentClient -> ConnId -> SndQueue -> C.APublicVerifyKey -> RetryInterval -> m () -> m () activateQueue c connId sq verifyKey retryInterval afterActivation = getActivation c connId >>= \case Nothing -> async runActivation >>= addActivation c connId @@ -603,11 +603,20 @@ notifyConnected :: AgentMonad m => AgentClient -> ConnId -> m () notifyConnected c connId = atomically $ writeTBQueue (subQ c) ("", connId, CON) newSndQueue :: - (MonadUnliftIO m, MonadReader Env m) => SMPQueueInfo -> m (SndQueue, SenderPublicKey, VerificationKey) -newSndQueue (SMPQueueInfo smpServer senderId encryptKey) = do + (MonadUnliftIO m, MonadReader Env m) => SMPQueueInfo -> m (SndQueue, SenderPublicKey, C.APublicVerifyKey) +newSndQueue qInfo = + asks (cmdSignAlg . config) >>= \case + C.SignAlg a -> newSndQueue_ a qInfo + +newSndQueue_ :: + (C.SignatureAlgorithm a, C.AlgorithmI a, MonadUnliftIO m, MonadReader Env m) => + C.SAlgorithm a -> + SMPQueueInfo -> + m (SndQueue, SenderPublicKey, C.APublicVerifyKey) +newSndQueue_ a (SMPQueueInfo smpServer senderId encryptKey) = do size <- asks $ rsaKeySize . config - (senderKey, sndPrivateKey) <- liftIO $ C.generateKeyPair size - (verifyKey, signKey) <- liftIO $ C.generateKeyPair size + (senderKey, sndPrivateKey) <- liftIO $ C.generateSignatureKeyPair size a + (verifyKey, signKey) <- liftIO $ C.generateSignatureKeyPair size C.SRSA let sndQueue = SndQueue { server = smpServer, diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index 031403a4a..9e86fe777 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -224,13 +224,23 @@ smpClientError = \case e -> INTERNAL $ show e newRcvQueue :: AgentMonad m => AgentClient -> SMPServer -> m (RcvQueue, SMPQueueInfo) -newRcvQueue c srv = do +newRcvQueue c srv = + asks (cmdSignAlg . config) >>= \case + C.SignAlg a -> newRcvQueue_ a c srv + +newRcvQueue_ :: + (C.SignatureAlgorithm a, C.AlgorithmI a, AgentMonad m) => + C.SAlgorithm a -> + AgentClient -> + SMPServer -> + m (RcvQueue, SMPQueueInfo) +newRcvQueue_ a c srv = do size <- asks $ rsaKeySize . config - (recipientKey, rcvPrivateKey) <- liftIO $ C.generateKeyPair size + (recipientKey, rcvPrivateKey) <- liftIO $ C.generateSignatureKeyPair size a logServer "-->" c srv "" "NEW" (rcvId, sId) <- withSMP c srv $ \smp -> createSMPQueue smp rcvPrivateKey recipientKey logServer "<--" c srv "" $ B.unwords ["IDS", logSecret rcvId, logSecret sId] - (encryptKey, decryptKey) <- liftIO $ C.generateKeyPair size + (encryptKey, decryptKey) <- liftIO $ C.generateEncryptionKeyPair size C.SRSA let rq = RcvQueue { server = srv, @@ -299,7 +309,7 @@ sendConfirmation c sq@SndQueue {server, sndId} senderKey cInfo = mkConfirmation :: SMPClient -> m MsgBody mkConfirmation smp = encryptAndSign smp sq . serializeSMPMessage $ SMPConfirmation senderKey cInfo -sendHello :: forall m. AgentMonad m => AgentClient -> SndQueue -> VerificationKey -> RetryInterval -> m () +sendHello :: forall m. AgentMonad m => AgentClient -> SndQueue -> C.APublicVerifyKey -> RetryInterval -> m () sendHello c sq@SndQueue {server, sndId, sndPrivateKey} verifyKey ri = withLogSMP_ c server sndId "SEND (retrying)" $ \smp -> do msg <- mkHello smp $ AckMode On @@ -350,23 +360,27 @@ encryptAndSign smp SndQueue {encryptKey, signKey} msg = do paddedSize <- asks $ (blockSize smp -) . reservedMsgSize liftError cryptoError $ do enc <- C.encrypt encryptKey paddedSize msg - C.Signature sig <- C.sign signKey enc - pure $ sig <> enc + sig <- C.sign signKey enc + pure $ C.signatureBytes sig <> enc decryptAndVerify :: AgentMonad m => RcvQueue -> ByteString -> m ByteString decryptAndVerify RcvQueue {decryptKey, verifyKey} msg = verifyMessage verifyKey msg >>= liftError cryptoError . C.decrypt decryptKey -verifyMessage :: AgentMonad m => Maybe VerificationKey -> ByteString -> m ByteString +verifyMessage :: AgentMonad m => Maybe C.APublicVerifyKey -> ByteString -> m ByteString verifyMessage verifyKey msg = do - size <- asks $ rsaKeySize . config - let (sig, enc) = B.splitAt size msg + sigSize <- asks $ rsaKeySize . config + let (s, enc) = B.splitAt sigSize msg case verifyKey of Nothing -> pure enc - Just k - | C.verify k (C.Signature sig) enc -> pure enc - | otherwise -> throwError $ AGENT A_SIGNATURE + Just k -> + case C.decodeSignature $ B.take (C.signatureSize k) s of + Left _ -> throwError $ AGENT A_SIGNATURE + Right sig -> + if C.verify k sig enc + then pure enc + else throwError $ AGENT A_SIGNATURE cryptoError :: C.CryptoError -> AgentErrorType cryptoError = \case diff --git a/src/Simplex/Messaging/Agent/Env/SQLite.hs b/src/Simplex/Messaging/Agent/Env/SQLite.hs index 6a063d4dd..1e9b367cd 100644 --- a/src/Simplex/Messaging/Agent/Env/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Env/SQLite.hs @@ -16,6 +16,7 @@ import Simplex.Messaging.Agent.RetryInterval import Simplex.Messaging.Agent.Store.SQLite import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations import Simplex.Messaging.Client +import qualified Simplex.Messaging.Crypto as C import System.Random (StdGen, newStdGen) import UnliftIO.STM @@ -23,6 +24,7 @@ data AgentConfig = AgentConfig { tcpPort :: ServiceName, smpServers :: NonEmpty SMPServer, rsaKeySize :: Int, + cmdSignAlg :: C.SignAlg, connIdBytes :: Int, tbqSize :: Natural, dbFile :: FilePath, @@ -41,6 +43,7 @@ defaultAgentConfig = { tcpPort = "5224", smpServers = undefined, rsaKeySize = 2048 `div` 8, + cmdSignAlg = C.SignAlg C.SEd448, connIdBytes = 12, tbqSize = 16, dbFile = "smp-agent.db", diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index 0614a2df8..09f9a3613 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -55,10 +55,6 @@ module Simplex.Messaging.Agent.Protocol MsgIntegrity (..), MsgErrorType (..), QueueStatus (..), - SignatureKey, - VerificationKey, - EncryptionKey, - DecryptionKey, ACorrId, AgentMsgId, @@ -223,7 +219,7 @@ data SMPMessage -- https://github.com/simplex-chat/simplexmq/blob/master/protocol/agent-protocol.md#messages-between-smp-agents data AMessage where -- | the first message in the queue to validate it is secured - HELLO :: VerificationKey -> AckMode -> AMessage + HELLO :: C.APublicVerifyKey -> AckMode -> AMessage -- | reply queue information REPLY :: SMPQueueInfo -> AMessage -- | agent envelope for the client message @@ -238,7 +234,7 @@ parseSMPMessage = parse (smpMessageP <* A.endOfLine) $ AGENT A_MESSAGE smpMessageP = A.endOfLine *> smpClientMessageP <|> smpConfirmationP smpConfirmationP :: Parser SMPMessage - smpConfirmationP = "KEY " *> (SMPConfirmation <$> C.pubKeyP <* A.endOfLine <* A.endOfLine <*> binaryBodyP <* A.endOfLine) + smpConfirmationP = "KEY " *> (SMPConfirmation <$> C.strKeyP <* A.endOfLine <* A.endOfLine <*> binaryBodyP <* A.endOfLine) smpClientMessageP :: Parser SMPMessage smpClientMessageP = @@ -253,7 +249,7 @@ parseSMPMessage = parse (smpMessageP <* A.endOfLine) $ AGENT A_MESSAGE -- | Serialize SMP message. serializeSMPMessage :: SMPMessage -> ByteString serializeSMPMessage = \case - SMPConfirmation sKey cInfo -> smpMessage ("KEY " <> C.serializePubKey sKey) "" (serializeBinary cInfo) <> "\n" + SMPConfirmation sKey cInfo -> smpMessage ("KEY " <> C.serializeKey sKey) "" (serializeBinary cInfo) <> "\n" SMPMessage {senderMsgId, senderTimestamp, previousMsgHash, agentMessage} -> let header = messageHeader senderMsgId senderTimestamp previousMsgHash body = serializeAgentMessage agentMessage @@ -269,7 +265,7 @@ agentMessageP = <|> "REPLY " *> reply <|> "MSG " *> a_msg where - hello = HELLO <$> C.pubKeyP <*> ackMode + hello = HELLO <$> C.strKeyP <*> ackMode reply = REPLY <$> smpQueueInfoP a_msg = A_MSG <$> binaryBodyP <* A.endOfLine ackMode = AckMode <$> (" NO_ACK" $> Off <|> pure On) @@ -277,7 +273,7 @@ agentMessageP = -- | SMP queue information parser. smpQueueInfoP :: Parser SMPQueueInfo smpQueueInfoP = - "smp::" *> (SMPQueueInfo <$> smpServerP <* "::" <*> base64P <* "::" <*> C.pubKeyP) + "smp::" *> (SMPQueueInfo <$> smpServerP <* "::" <*> base64P <* "::" <*> C.strKeyP) -- | SMP server location parser. smpServerP :: Parser SMPServer @@ -289,14 +285,14 @@ smpServerP = SMPServer <$> server <*> optional port <*> optional kHash serializeAgentMessage :: AMessage -> ByteString serializeAgentMessage = \case - HELLO verifyKey ackMode -> "HELLO " <> C.serializePubKey verifyKey <> if ackMode == AckMode Off then " NO_ACK" else "" + HELLO verifyKey ackMode -> "HELLO " <> C.serializeKey verifyKey <> if ackMode == AckMode Off then " NO_ACK" else "" REPLY qInfo -> "REPLY " <> serializeSmpQueueInfo qInfo A_MSG body -> "MSG " <> serializeBinary body <> "\n" -- | Serialize SMP queue information that is sent out-of-band. serializeSmpQueueInfo :: SMPQueueInfo -> ByteString serializeSmpQueueInfo (SMPQueueInfo srv qId ek) = - B.intercalate "::" ["smp", serializeServer srv, encode qId, C.serializePubKey ek] + B.intercalate "::" ["smp", serializeServer srv, encode qId, C.serializeKey ek] -- | Serialize SMP server location. serializeServer :: SMPServer -> ByteString @@ -332,21 +328,9 @@ newtype AckMode = AckMode OnOff deriving (Eq, Show) -- | SMP queue information sent out-of-band. -- -- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#out-of-band-messages -data SMPQueueInfo = SMPQueueInfo SMPServer SMP.SenderId EncryptionKey +data SMPQueueInfo = SMPQueueInfo SMPServer SMP.SenderId C.APublicEncryptKey deriving (Eq, Show) --- | Public key used to E2E encrypt SMP messages. -type EncryptionKey = C.PublicKey - --- | Private key used to E2E decrypt SMP messages. -type DecryptionKey = C.PrivateKey - --- | Private key used to sign SMP commands -type SignatureKey = C.PrivateKey - --- | Public key used by SMP server to authorize (verify) SMP commands. -type VerificationKey = C.PublicKey - data QueueDirection = SND | RCV deriving (Show) -- | SMP queue status. @@ -437,7 +421,7 @@ data SMPAgentError A_PROHIBITED | -- | cannot RSA/AES-decrypt or parse decrypted header A_ENCRYPTION - | -- | invalid RSA signature + | -- | invalid signature A_SIGNATURE deriving (Eq, Generic, Read, Show, Exception) diff --git a/src/Simplex/Messaging/Agent/Store.hs b/src/Simplex/Messaging/Agent/Store.hs index f6bdec954..2eba1b814 100644 --- a/src/Simplex/Messaging/Agent/Store.hs +++ b/src/Simplex/Messaging/Agent/Store.hs @@ -18,6 +18,7 @@ import Data.Kind (Type) import Data.Time (UTCTime) import Data.Type.Equality import Simplex.Messaging.Agent.Protocol +import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Protocol ( MsgBody, MsgId, @@ -41,7 +42,7 @@ class Monad m => MonadAgentStore s m where upgradeRcvConnToDuplex :: s -> ConnId -> SndQueue -> m () upgradeSndConnToDuplex :: s -> ConnId -> RcvQueue -> m () setRcvQueueStatus :: s -> RcvQueue -> QueueStatus -> m () - setRcvQueueActive :: s -> RcvQueue -> VerificationKey -> m () + setRcvQueueActive :: s -> RcvQueue -> C.APublicVerifyKey -> m () setSndQueueStatus :: s -> SndQueue -> QueueStatus -> m () -- Confirmations @@ -70,8 +71,8 @@ data RcvQueue = RcvQueue rcvId :: SMP.RecipientId, rcvPrivateKey :: RecipientPrivateKey, sndId :: Maybe SMP.SenderId, - decryptKey :: DecryptionKey, - verifyKey :: Maybe VerificationKey, + decryptKey :: C.APrivateDecryptKey, + verifyKey :: Maybe C.APublicVerifyKey, status :: QueueStatus } deriving (Eq, Show) @@ -81,8 +82,8 @@ data SndQueue = SndQueue { server :: SMPServer, sndId :: SMP.SenderId, sndPrivateKey :: SenderPrivateKey, - encryptKey :: EncryptionKey, - signKey :: SignatureKey, + encryptKey :: C.APublicEncryptKey, + signKey :: C.APrivateSignKey, status :: QueueStatus } deriving (Eq, Show) diff --git a/src/Simplex/Messaging/Agent/Store/SQLite.hs b/src/Simplex/Messaging/Agent/Store/SQLite.hs index 177dd2950..7e2f09bdc 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite.hs @@ -51,6 +51,7 @@ import Simplex.Messaging.Agent.Protocol 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.Parsers (blobFieldParser) import Simplex.Messaging.Protocol (MsgBody) import qualified Simplex.Messaging.Protocol as SMP @@ -248,7 +249,7 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto |] [":status" := status, ":host" := host, ":port" := serializePort_ port, ":rcv_id" := rcvId] - setRcvQueueActive :: SQLiteStore -> RcvQueue -> VerificationKey -> m () + setRcvQueueActive :: SQLiteStore -> RcvQueue -> C.APublicVerifyKey -> m () setRcvQueueActive st RcvQueue {rcvId, server = SMPServer {host, port}} verifyKey = -- ? throw error if queue does not exist? liftIO . withTransaction st $ \db -> diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index 7c845d809..0dc0ac929 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -82,7 +82,7 @@ data SMPClient = SMPClient tcpTimeout :: Int, clientCorrId :: TVar Natural, sentCommands :: TVar (Map CorrId Request), - sndQ :: TBQueue SignedRawTransmission, + sndQ :: TBQueue SentRawTransmission, rcvQ :: TBQueue SignedTransmissionOrError, msgQ :: TBQueue SMPServerTransmission, blockSize :: Int @@ -333,14 +333,14 @@ suspendSMPQueue = okSMPCommand $ Cmd SRecipient OFF deleteSMPQueue :: SMPClient -> RecipientPrivateKey -> QueueId -> ExceptT SMPClientError IO () deleteSMPQueue = okSMPCommand $ Cmd SRecipient DEL -okSMPCommand :: Cmd -> SMPClient -> C.PrivateKey -> QueueId -> ExceptT SMPClientError IO () +okSMPCommand :: Cmd -> SMPClient -> C.APrivateSignKey -> QueueId -> ExceptT SMPClientError IO () okSMPCommand cmd c pKey qId = sendSMPCommand c (Just pKey) qId cmd >>= \case Cmd _ OK -> return () _ -> throwE SMPUnexpectedResponse -- | Send any SMP command ('Cmd' type). -sendSMPCommand :: SMPClient -> Maybe C.PrivateKey -> QueueId -> Cmd -> ExceptT SMPClientError IO Cmd +sendSMPCommand :: SMPClient -> Maybe C.APrivateSignKey -> QueueId -> Cmd -> ExceptT SMPClientError IO Cmd sendSMPCommand SMPClient {sndQ, sentCommands, clientCorrId, tcpTimeout} pKey qId cmd = do corrId <- lift_ getNextCorrId t <- signTransmission $ serializeTransmission (corrId, qId, cmd) @@ -354,20 +354,20 @@ sendSMPCommand SMPClient {sndQ, sentCommands, clientCorrId, tcpTimeout} pKey qId i <- stateTVar clientCorrId $ \i -> (i, i + 1) pure . CorrId $ bshow i - signTransmission :: ByteString -> ExceptT SMPClientError IO SignedRawTransmission + signTransmission :: ByteString -> ExceptT SMPClientError IO SentRawTransmission signTransmission t = case pKey of - Nothing -> return ("", t) + Nothing -> return (Nothing, t) Just pk -> do sig <- liftError SMPSignatureError $ C.sign pk t - return (sig, t) + return (Just sig, t) -- two separate "atomically" needed to avoid blocking - sendRecv :: CorrId -> SignedRawTransmission -> IO Response + sendRecv :: CorrId -> SentRawTransmission -> IO Response sendRecv corrId t = atomically (send corrId t) >>= withTimeout . atomically . takeTMVar where withTimeout a = fromMaybe (Left SMPResponseTimeout) <$> timeout tcpTimeout a - send :: CorrId -> SignedRawTransmission -> STM (TMVar Response) + send :: CorrId -> SentRawTransmission -> STM (TMVar Response) send corrId t = do r <- newEmptyTMVar modifyTVar sentCommands . M.insert corrId $ Request qId r diff --git a/src/Simplex/Messaging/Crypto.hs b/src/Simplex/Messaging/Crypto.hs index a92c75ffd..b4ffbc9ee 100644 --- a/src/Simplex/Messaging/Crypto.hs +++ b/src/Simplex/Messaging/Crypto.hs @@ -1,11 +1,19 @@ {-# LANGUAGE AllowAmbiguousTypes #-} +{-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE UndecidableInstances #-} +{-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-} -- | -- Module : Simplex.Messaging.Crypto @@ -19,28 +27,50 @@ -- This module provides cryptography implementation for SMP protocols based on -- . module Simplex.Messaging.Crypto - ( -- * RSA keys + ( -- * Cryptographic keys + Algorithm (..), + SAlgorithm (..), + Alg (..), + SignAlg (..), PrivateKey (..), PublicKey (..), + APrivateKey (..), + APublicKey (..), + APrivateSignKey (..), + APublicVerifyKey (..), + APrivateDecryptKey (..), + APublicEncryptKey (..), + CryptoKey (..), + CryptoPrivateKey (..), KeyPair, KeyHash (..), generateKeyPair, - publicKey', - publicKeySize, - validKeySize, + generateKeyPair', + generateSignatureKeyPair, + generateEncryptionKeyPair, + privateToX509, -- * E2E hybrid encryption scheme encrypt, + encrypt', decrypt, + decrypt', -- * RSA OAEP encryption encryptOAEP, decryptOAEP, - -- * RSA PSS signing + -- * sign/verify Signature (..), + ASignature (..), + CryptoSignature (..), + SignatureSize (..), + SignatureAlgorithm, + AlgorithmI (..), sign, verify, + verify', + validSignatureSize, -- * AES256 AEAD-GCM scheme Key (..), @@ -56,13 +86,7 @@ module Simplex.Messaging.Crypto ivP, -- * Encoding of RSA keys - serializePrivKey, - serializePubKey, - encodePubKey, publicKeyHash, - privKeyP, - pubKeyP, - binaryPubKeyP, -- * SHA256 hash sha256Hash, @@ -81,6 +105,10 @@ import qualified Crypto.Error as CE import Crypto.Hash (Digest, SHA256 (..), hash) import Crypto.Number.Generate (generateMax) import Crypto.Number.Prime (findPrimeFrom) +import qualified Crypto.PubKey.Curve25519 as X25519 +import qualified Crypto.PubKey.Curve448 as X448 +import qualified Crypto.PubKey.Ed25519 as Ed25519 +import qualified Crypto.PubKey.Ed448 as Ed448 import qualified Crypto.PubKey.RSA as R import qualified Crypto.PubKey.RSA.OAEP as OAEP import qualified Crypto.PubKey.RSA.PSS as PSS @@ -97,42 +125,507 @@ import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.ByteString.Internal (c2w, w2c) import Data.ByteString.Lazy (fromStrict, toStrict) +import Data.Constraint (Dict (..)) +import Data.Kind (Constraint, Type) import Data.String +import Data.Type.Equality import Data.X509 import Database.SQLite.Simple.FromField (FromField (..)) import Database.SQLite.Simple.ToField (ToField (..)) +import GHC.TypeLits (ErrorMessage (..), TypeError) import Network.Transport.Internal (decodeWord32, encodeWord32) import Simplex.Messaging.Parsers (base64P, blobFieldParser, parseAll, parseString) import Simplex.Messaging.Util (liftEitherError, (<$?>)) --- | A newtype of 'Crypto.PubKey.RSA.PublicKey'. -newtype PublicKey = PublicKey {rsaPublicKey :: R.PublicKey} deriving (Eq, Show) +-- | Cryptographic algorithms. +data Algorithm = RSA | Ed25519 | Ed448 | X25519 | X448 --- | A newtype of 'Crypto.PubKey.RSA.PrivateKey' (PublicKey may be inside). -newtype PrivateKey = PrivateKey {rsaPrivateKey :: R.PrivateKey} deriving (Eq, Show) +-- | Singleton types for 'Algorithm'. +data SAlgorithm :: Algorithm -> Type where + SRSA :: SAlgorithm RSA + SEd25519 :: SAlgorithm Ed25519 + SEd448 :: SAlgorithm Ed448 + SX25519 :: SAlgorithm X25519 + SX448 :: SAlgorithm X448 -instance IsString PrivateKey where - fromString = parseString $ decode >=> decodePrivKey +deriving instance Eq (SAlgorithm a) -instance IsString PublicKey where - fromString = parseString $ decode >=> decodePubKey +deriving instance Show (SAlgorithm a) -instance ToField PrivateKey where toField = toField . encodePrivKey +data Alg = forall a. AlgorithmI a => Alg (SAlgorithm a) -instance ToField PublicKey where toField = toField . encodePubKey +data SignAlg + = forall a. + (AlgorithmI a, SignatureAlgorithm a) => + SignAlg (SAlgorithm a) -instance FromField PrivateKey where fromField = blobFieldParser binaryPrivKeyP +class AlgorithmI (a :: Algorithm) where sAlgorithm :: SAlgorithm a -instance FromField PublicKey where fromField = blobFieldParser binaryPubKeyP +instance AlgorithmI RSA where sAlgorithm = SRSA + +instance AlgorithmI Ed25519 where sAlgorithm = SEd25519 + +instance AlgorithmI Ed448 where sAlgorithm = SEd448 + +instance AlgorithmI X25519 where sAlgorithm = SX25519 + +instance AlgorithmI X448 where sAlgorithm = SX448 + +instance TestEquality SAlgorithm where + testEquality SRSA SRSA = Just Refl + testEquality SEd25519 SEd25519 = Just Refl + testEquality SEd448 SEd448 = Just Refl + testEquality SX25519 SX25519 = Just Refl + testEquality SX448 SX448 = Just Refl + testEquality _ _ = Nothing + +-- | GADT for public keys. +data PublicKey (a :: Algorithm) where + PublicKeyRSA :: R.PublicKey -> PublicKey RSA + PublicKeyEd25519 :: Ed25519.PublicKey -> PublicKey Ed25519 + PublicKeyEd448 :: Ed448.PublicKey -> PublicKey Ed448 + PublicKeyX25519 :: X25519.PublicKey -> PublicKey X25519 + PublicKeyX448 :: X448.PublicKey -> PublicKey X448 + +deriving instance Eq (PublicKey a) + +deriving instance Show (PublicKey a) + +data APublicKey + = forall a. + AlgorithmI a => + APublicKey (SAlgorithm a) (PublicKey a) + +instance Eq APublicKey where + APublicKey a k == APublicKey a' k' = case testEquality a a' of + Just Refl -> k == k' + Nothing -> False + +deriving instance Show APublicKey + +-- | GADT for private keys. +data PrivateKey (a :: Algorithm) where + PrivateKeyRSA :: {privateKeyRSA :: R.PrivateKey} -> PrivateKey RSA + PrivateKeyEd25519 :: Ed25519.SecretKey -> Ed25519.PublicKey -> PrivateKey Ed25519 + PrivateKeyEd448 :: Ed448.SecretKey -> Ed448.PublicKey -> PrivateKey Ed448 + PrivateKeyX25519 :: X25519.SecretKey -> PrivateKey X25519 + PrivateKeyX448 :: X448.SecretKey -> PrivateKey X448 + +deriving instance Eq (PrivateKey a) + +deriving instance Show (PrivateKey a) + +data APrivateKey + = forall a. + AlgorithmI a => + APrivateKey (SAlgorithm a) (PrivateKey a) + +instance Eq APrivateKey where + APrivateKey a k == APrivateKey a' k' = case testEquality a a' of + Just Refl -> k == k' + Nothing -> False + +deriving instance Show APrivateKey + +class AlgorithmPrefix k where + algorithmPrefix :: k -> ByteString + +instance AlgorithmPrefix (SAlgorithm a) where + algorithmPrefix = \case + SRSA -> "rsa" + SEd25519 -> "ed25519" + SEd448 -> "ed448" + SX25519 -> "x25519" + SX448 -> "x448" + +instance AlgorithmI a => AlgorithmPrefix (PublicKey a) where + algorithmPrefix _ = algorithmPrefix $ sAlgorithm @a + +instance AlgorithmI a => AlgorithmPrefix (PrivateKey a) where + algorithmPrefix _ = algorithmPrefix $ sAlgorithm @a + +instance AlgorithmPrefix APublicKey where + algorithmPrefix (APublicKey a _) = algorithmPrefix a + +instance AlgorithmPrefix APrivateKey where + algorithmPrefix (APrivateKey a _) = algorithmPrefix a + +prefixAlgorithm :: ByteString -> Either String Alg +prefixAlgorithm = \case + "rsa" -> Right $ Alg SRSA + "ed25519" -> Right $ Alg SEd25519 + "ed448" -> Right $ Alg SEd448 + "x25519" -> Right $ Alg SX25519 + "x448" -> Right $ Alg SX448 + _ -> Left "unknown algorithm" + +algP :: Parser Alg +algP = prefixAlgorithm <$?> A.takeTill (== ':') + +type family SignatureAlgorithm (a :: Algorithm) :: Constraint where + SignatureAlgorithm RSA = () + SignatureAlgorithm Ed25519 = () + SignatureAlgorithm Ed448 = () + SignatureAlgorithm a = + (Int ~ Bool, TypeError (Text "Algorithm " :<>: ShowType a :<>: Text " cannot be used to sign/verify")) + +signatureAlgorithm :: SAlgorithm a -> Maybe (Dict (SignatureAlgorithm a)) +signatureAlgorithm = \case + SRSA -> Just Dict + SEd25519 -> Just Dict + SEd448 -> Just Dict + _ -> Nothing + +data APrivateSignKey + = forall a. + (AlgorithmI a, SignatureAlgorithm a) => + APrivateSignKey (SAlgorithm a) (PrivateKey a) + +instance Eq APrivateSignKey where + APrivateSignKey a k == APrivateSignKey a' k' = case testEquality a a' of + Just Refl -> k == k' + Nothing -> False + +deriving instance Show APrivateSignKey + +data APublicVerifyKey + = forall a. + (AlgorithmI a, SignatureAlgorithm a) => + APublicVerifyKey (SAlgorithm a) (PublicKey a) + +instance Eq APublicVerifyKey where + APublicVerifyKey a k == APublicVerifyKey a' k' = case testEquality a a' of + Just Refl -> k == k' + Nothing -> False + +deriving instance Show APublicVerifyKey + +type family EncryptionAlgorithm (a :: Algorithm) :: Constraint where + EncryptionAlgorithm RSA = () + EncryptionAlgorithm a = + (Int ~ Bool, TypeError (Text "Algorithm " :<>: ShowType a :<>: Text " cannot be used to encrypt/decrypt")) + +encryptionAlgorithm :: SAlgorithm a -> Maybe (Dict (EncryptionAlgorithm a)) +encryptionAlgorithm = \case + SRSA -> Just Dict + _ -> Nothing + +data APrivateDecryptKey + = forall a. + (AlgorithmI a, EncryptionAlgorithm a) => + APrivateDecryptKey (SAlgorithm a) (PrivateKey a) + +instance Eq APrivateDecryptKey where + APrivateDecryptKey a k == APrivateDecryptKey a' k' = case testEquality a a' of + Just Refl -> k == k' + Nothing -> False + +deriving instance Show APrivateDecryptKey + +data APublicEncryptKey + = forall a. + (AlgorithmI a, EncryptionAlgorithm a) => + APublicEncryptKey (SAlgorithm a) (PublicKey a) + +instance Eq APublicEncryptKey where + APublicEncryptKey a k == APublicEncryptKey a' k' = case testEquality a a' of + Just Refl -> k == k' + Nothing -> False + +deriving instance Show APublicEncryptKey + +-- | Class for all key types +class CryptoKey k where + keySize :: k -> Int + + validKeySize :: k -> Bool + + -- | base64 X509 key encoding with algorithm prefix + serializeKey :: k -> ByteString + + -- | binary X509 key encoding + encodeKey :: k -> ByteString + + -- | base64 X509 (with algorithm prefix) key parser + strKeyP :: Parser k + + -- | binary X509 key parser + binaryKeyP :: Parser k + +-- | X509 encoding of any public key. +instance CryptoKey APublicKey where + keySize (APublicKey _ k) = keySize k + validKeySize (APublicKey _ k) = validKeySize k + serializeKey (APublicKey _ k) = serializeKey k + encodeKey (APublicKey _ k) = encodeKey k + strKeyP = do + Alg a <- algP <* A.char ':' + k@(APublicKey a' _) <- decodePubKey <$?> base64P + case testEquality a a' of + Just Refl -> pure k + _ -> fail $ "public key algorithm " <> show a <> " does not match prefix" + binaryKeyP = decodePubKey <$?> A.takeByteString + +-- | X509 encoding of signature public key. +instance CryptoKey APublicVerifyKey where + keySize (APublicVerifyKey _ k) = keySize k + validKeySize (APublicVerifyKey _ k) = validKeySize k + serializeKey (APublicVerifyKey _ k) = serializeKey k + encodeKey (APublicVerifyKey _ k) = encodeKey k + strKeyP = pubVerifyKey <$?> strKeyP + binaryKeyP = pubVerifyKey <$?> binaryKeyP + +-- | X509 encoding of encryption public key. +instance CryptoKey APublicEncryptKey where + keySize (APublicEncryptKey _ k) = keySize k + validKeySize (APublicEncryptKey _ k) = validKeySize k + serializeKey (APublicEncryptKey _ k) = serializeKey k + encodeKey (APublicEncryptKey _ k) = encodeKey k + strKeyP = pubEncryptKey <$?> strKeyP + binaryKeyP = pubEncryptKey <$?> binaryKeyP + +-- | X509 encoding of 'PublicKey'. +instance forall a. AlgorithmI a => CryptoKey (PublicKey a) where + keySize = \case + PublicKeyRSA k -> R.public_size k + PublicKeyEd25519 _ -> Ed25519.publicKeySize + PublicKeyEd448 _ -> Ed448.publicKeySize + PublicKeyX25519 _ -> x25519_size + PublicKeyX448 _ -> x448_size + validKeySize = \case + PublicKeyRSA k -> validRSAKeySize $ R.public_size k + _ -> True + serializeKey k = algorithmPrefix k <> ":" <> encode (encodeKey k) + encodeKey = encodeASNKey . publicToX509 + strKeyP = pubKey' <$?> strKeyP + binaryKeyP = pubKey' <$?> binaryKeyP + +-- | X509 encoding of any private key. +instance CryptoKey APrivateKey where + keySize (APrivateKey _ k) = keySize k + validKeySize (APrivateKey _ k) = validKeySize k + serializeKey (APrivateKey _ k) = serializeKey k + encodeKey (APrivateKey _ k) = encodeKey k + strKeyP = do + Alg a <- algP <* A.char ':' + k@(APrivateKey a' _) <- decodePrivKey <$?> base64P + case testEquality a a' of + Just Refl -> pure k + _ -> fail $ "private key algorithm " <> show a <> " does not match prefix" + binaryKeyP = decodePrivKey <$?> A.takeByteString + +-- | X509 encoding of signature private key. +instance CryptoKey APrivateSignKey where + keySize (APrivateSignKey _ k) = keySize k + validKeySize (APrivateSignKey _ k) = validKeySize k + serializeKey (APrivateSignKey _ k) = serializeKey k + encodeKey (APrivateSignKey _ k) = encodeKey k + strKeyP = privSignKey <$?> strKeyP + binaryKeyP = privSignKey <$?> binaryKeyP + +-- | X509 encoding of encryption private key. +instance CryptoKey APrivateDecryptKey where + keySize (APrivateDecryptKey _ k) = keySize k + validKeySize (APrivateDecryptKey _ k) = validKeySize k + serializeKey (APrivateDecryptKey _ k) = serializeKey k + encodeKey (APrivateDecryptKey _ k) = encodeKey k + strKeyP = privDecryptKey <$?> strKeyP + binaryKeyP = privDecryptKey <$?> binaryKeyP + +-- | X509 encoding of 'PrivateKey'. +instance AlgorithmI a => CryptoKey (PrivateKey a) where + keySize = \case + PrivateKeyRSA k -> rsaPrivateKeySize k + PrivateKeyEd25519 _ _ -> Ed25519.secretKeySize + PrivateKeyEd448 _ _ -> Ed448.secretKeySize + PrivateKeyX25519 _ -> x25519_size + PrivateKeyX448 _ -> x448_size + validKeySize = \case + PrivateKeyRSA k -> validRSAKeySize $ rsaPrivateKeySize k + _ -> True + serializeKey k = algorithmPrefix k <> ":" <> encode (encodeKey k) + encodeKey = encodeASNKey . privateToX509 + strKeyP = privKey' <$?> strKeyP + binaryKeyP = privKey' <$?> binaryKeyP + +type family PublicKeyType pk where + PublicKeyType APrivateKey = APublicKey + PublicKeyType APrivateSignKey = APublicVerifyKey + PublicKeyType APrivateDecryptKey = APublicEncryptKey + PublicKeyType (PrivateKey a) = PublicKey a + +class CryptoPrivateKey pk where publicKey :: pk -> PublicKeyType pk + +instance CryptoPrivateKey APrivateKey where + publicKey (APrivateKey a k) = APublicKey a $ publicKey k + +instance CryptoPrivateKey APrivateSignKey where + publicKey (APrivateSignKey a k) = APublicVerifyKey a $ publicKey k + +instance CryptoPrivateKey APrivateDecryptKey where + publicKey (APrivateDecryptKey a k) = APublicEncryptKey a $ publicKey k + +instance CryptoPrivateKey (PrivateKey a) where + publicKey = \case + PrivateKeyRSA k -> PublicKeyRSA $ R.private_pub k + PrivateKeyEd25519 _ k -> PublicKeyEd25519 k + PrivateKeyEd448 _ k -> PublicKeyEd448 k + PrivateKeyX25519 k -> PublicKeyX25519 $ X25519.toPublic k + PrivateKeyX448 k -> PublicKeyX448 $ X448.toPublic k + +instance AlgorithmI a => IsString (PrivateKey a) where + fromString = parseString $ decode >=> decodePrivKey >=> privKey' + +instance AlgorithmI a => IsString (PublicKey a) where + fromString = parseString $ decode >=> decodePubKey >=> pubKey' -- | Tuple of RSA 'PublicKey' and 'PrivateKey'. -type KeyPair = (PublicKey, PrivateKey) +type KeyPair a = (PublicKey a, PrivateKey a) --- | RSA signature newtype. -newtype Signature = Signature {unSignature :: ByteString} deriving (Eq, Show) +type AKeyPair = (APublicKey, APrivateKey) -instance IsString Signature where - fromString = Signature . fromString +type ASignatureKeyPair = (APublicVerifyKey, APrivateSignKey) + +type AnEncryptionKeyPair = (APublicEncryptKey, APrivateDecryptKey) + +generateKeyPair :: AlgorithmI a => Int -> SAlgorithm a -> IO AKeyPair +generateKeyPair size a = bimap (APublicKey a) (APrivateKey a) <$> generateKeyPair' size a + +generateSignatureKeyPair :: + (AlgorithmI a, SignatureAlgorithm a) => Int -> SAlgorithm a -> IO ASignatureKeyPair +generateSignatureKeyPair size a = + bimap (APublicVerifyKey a) (APrivateSignKey a) <$> generateKeyPair' size a + +generateEncryptionKeyPair :: + (AlgorithmI a, EncryptionAlgorithm a) => Int -> SAlgorithm a -> IO AnEncryptionKeyPair +generateEncryptionKeyPair size a = + bimap (APublicEncryptKey a) (APrivateDecryptKey a) <$> generateKeyPair' size a + +generateKeyPair' :: Int -> SAlgorithm a -> IO (KeyPair a) +generateKeyPair' size = \case + SRSA -> generateKeyPairRSA size + SEd25519 -> + Ed25519.generateSecretKey >>= \pk -> + let k = Ed25519.toPublic pk + in pure (PublicKeyEd25519 k, PrivateKeyEd25519 pk k) + SEd448 -> + Ed448.generateSecretKey >>= \pk -> + let k = Ed448.toPublic pk + in pure (PublicKeyEd448 k, PrivateKeyEd448 pk k) + SX25519 -> + X25519.generateSecretKey >>= \pk -> + let k = X25519.toPublic pk + in pure (PublicKeyX25519 k, PrivateKeyX25519 pk) + SX448 -> + X448.generateSecretKey >>= \pk -> + let k = X448.toPublic pk + in pure (PublicKeyX448 k, PrivateKeyX448 pk) + +instance ToField APrivateSignKey where toField = toField . encodeKey + +instance ToField APublicVerifyKey where toField = toField . encodeKey + +instance ToField APrivateDecryptKey where toField = toField . encodeKey + +instance ToField APublicEncryptKey where toField = toField . encodeKey + +instance FromField APrivateSignKey where fromField = blobFieldParser binaryKeyP + +instance FromField APublicVerifyKey where fromField = blobFieldParser binaryKeyP + +instance FromField APrivateDecryptKey where fromField = blobFieldParser binaryKeyP + +instance FromField APublicEncryptKey where fromField = blobFieldParser binaryKeyP + +instance IsString (Maybe ASignature) where + fromString = parseString $ decode >=> decodeSignature + +data Signature (a :: Algorithm) where + SignatureRSA :: ByteString -> Signature RSA + SignatureEd25519 :: Ed25519.Signature -> Signature Ed25519 + SignatureEd448 :: Ed448.Signature -> Signature Ed448 + +deriving instance Eq (Signature a) + +deriving instance Show (Signature a) + +data ASignature + = forall a. + (AlgorithmI a, SignatureAlgorithm a) => + ASignature (SAlgorithm a) (Signature a) + +instance Eq ASignature where + ASignature a s == ASignature a' s' = case testEquality a a' of + Just Refl -> s == s' + _ -> False + +deriving instance Show ASignature + +class CryptoSignature s where + serializeSignature :: s -> ByteString + serializeSignature = encode . signatureBytes + signatureBytes :: s -> ByteString + decodeSignature :: ByteString -> Either String s + +instance CryptoSignature ASignature where + signatureBytes (ASignature _ sig) = signatureBytes sig + decodeSignature s + | l == Ed25519.signatureSize = + ASignature SEd25519 . SignatureEd25519 <$> ed Ed25519.signature s + | l == Ed448.signatureSize = + ASignature SEd448 . SignatureEd448 <$> ed Ed448.signature s + | l == 128 || l == 256 || l == 384 || l == 512 = rsa s + | otherwise = Left "bad signature size" + where + l = B.length s + ed alg = first show . CE.eitherCryptoError . alg + rsa = Right . ASignature SRSA . SignatureRSA + +instance CryptoSignature (Maybe ASignature) where + signatureBytes = maybe "" signatureBytes + decodeSignature s + | B.null s = Right Nothing + | otherwise = Just <$> decodeSignature s + +instance AlgorithmI a => CryptoSignature (Signature a) where + signatureBytes = \case + SignatureRSA s -> s + SignatureEd25519 s -> BA.convert s + SignatureEd448 s -> BA.convert s + decodeSignature s = do + ASignature a sig <- decodeSignature s + case testEquality a $ sAlgorithm @a of + Just Refl -> Right sig + _ -> Left "bad signature algorithm" + +class SignatureSize s where signatureSize :: s -> Int + +instance SignatureSize (Signature a) where + signatureSize = \case + SignatureRSA s -> B.length s + SignatureEd25519 _ -> Ed25519.signatureSize + SignatureEd448 _ -> Ed448.signatureSize + +instance SignatureSize APrivateSignKey where + signatureSize (APrivateSignKey _ k) = signatureSize k + +instance SignatureSize APublicVerifyKey where + signatureSize (APublicVerifyKey _ k) = signatureSize k + +instance SignatureAlgorithm a => SignatureSize (PrivateKey a) where + signatureSize = \case + PrivateKeyRSA k -> rsaPrivateKeySize k + PrivateKeyEd25519 _ _ -> Ed25519.signatureSize + PrivateKeyEd448 _ _ -> Ed448.signatureSize + +instance SignatureAlgorithm a => SignatureSize (PublicKey a) where + signatureSize = \case + PublicKeyRSA k -> R.public_size k + PublicKeyEd25519 _ -> Ed25519.signatureSize + PublicKeyEd448 _ -> Ed448.signatureSize + +rsaPrivateKeySize :: R.PrivateKey -> Int +rsaPrivateKeySize = R.public_size . R.private_pub -- | Various cryptographic or related errors. data CryptoError @@ -142,6 +635,8 @@ data CryptoError RSADecryptError R.Error | -- | RSA PSS signature error RSASignError R.Error + | -- | Unsupported signing algorithm + UnsupportedAlgorithm | -- | AES initialization error AESCipherError CE.CryptoError | -- | IV generation error @@ -164,8 +659,8 @@ authTagSize :: Int authTagSize = 128 `div` 8 -- | Generate RSA key pair. -generateKeyPair :: Int -> IO KeyPair -generateKeyPair size = loop +generateKeyPairRSA :: Int -> IO (KeyPair RSA) +generateKeyPairRSA size = loop where publicExponent = findPrimeFrom . (+ 3) <$> generateMax pubExpRange loop = do @@ -174,24 +669,20 @@ generateKeyPair size = loop d = R.private_d pk if d * d < n then loop - else pure (PublicKey k, PrivateKey pk) + else pure (PublicKeyRSA k, PrivateKeyRSA pk) -privateKeySize :: PrivateKey -> Int -privateKeySize = R.public_size . R.private_pub . rsaPrivateKey +x25519_size :: Int +x25519_size = 32 -publicKey' :: PrivateKey -> PublicKey -publicKey' = PublicKey . R.private_pub . rsaPrivateKey +x448_size :: Int +x448_size = 448 `quot` 8 -publicKeySize :: PublicKey -> Int -publicKeySize = R.public_size . rsaPublicKey +validRSAKeySize :: Int -> Bool +validRSAKeySize n = n == 128 || n == 256 || n == 384 || n == 512 -validKeySize :: Int -> Bool -validKeySize = \case - 128 -> True - 256 -> True - 384 -> True - 512 -> True - _ -> False +validSignatureSize :: Int -> Bool +validSignatureSize n = + n == Ed25519.signatureSize || n == Ed448.signatureSize || validRSAKeySize n data Header = Header { aesKey :: Key, @@ -217,8 +708,8 @@ instance ToField KeyHash where toField = toField . encode . unKeyHash instance FromField KeyHash where fromField = blobFieldParser $ KeyHash <$> base64P -- | Digest (hash) of binary X509 encoding of RSA public key. -publicKeyHash :: PublicKey -> KeyHash -publicKeyHash = KeyHash . sha256Hash . encodePubKey +publicKeyHash :: PublicKey RSA -> KeyHash +publicKeyHash = KeyHash . sha256Hash . encodeKey -- | SHA256 digest. sha256Hash :: ByteString -> ByteString @@ -249,28 +740,36 @@ parseHeader = first CryptoHeaderError . parseAll headerP -- * E2E hybrid encryption scheme --- | E2E encrypt SMP agent messages. +-- | Legacy hybrid E2E encryption of SMP agent messages (RSA-OAEP/AES-256-GCM-SHA256). -- -- https://github.com/simplex-chat/simplexmq/blob/master/rfcs/2021-01-26-crypto.md#e2e-encryption -encrypt :: PublicKey -> Int -> ByteString -> ExceptT CryptoError IO ByteString -encrypt k paddedSize msg = do +encrypt' :: PublicKey a -> Int -> ByteString -> ExceptT CryptoError IO ByteString +encrypt' k@(PublicKeyRSA _) paddedSize msg = do aesKey <- liftIO randomAesKey ivBytes <- liftIO randomIV (authTag, msg') <- encryptAES aesKey ivBytes paddedSize msg let header = Header {aesKey, ivBytes, authTag, msgSize = B.length msg} encHeader <- encryptOAEP k $ serializeHeader header return $ encHeader <> msg' +encrypt' _ _ _ = throwE UnsupportedAlgorithm --- | E2E decrypt SMP agent messages. +-- | Legacy hybrid E2E decryption of SMP agent messages (RSA-OAEP/AES-256-GCM-SHA256). -- -- https://github.com/simplex-chat/simplexmq/blob/master/rfcs/2021-01-26-crypto.md#e2e-encryption -decrypt :: PrivateKey -> ByteString -> ExceptT CryptoError IO ByteString -decrypt pk msg'' = do - let (encHeader, msg') = B.splitAt (privateKeySize pk) msg'' +decrypt' :: PrivateKey a -> ByteString -> ExceptT CryptoError IO ByteString +decrypt' pk@(PrivateKeyRSA _) msg'' = do + let (encHeader, msg') = B.splitAt (keySize pk) msg'' header <- decryptOAEP pk encHeader Header {aesKey, ivBytes, authTag, msgSize} <- except $ parseHeader header msg <- decryptAES aesKey ivBytes msg' authTag return $ B.take msgSize msg +decrypt' _ _ = throwE UnsupportedAlgorithm + +encrypt :: APublicEncryptKey -> Int -> ByteString -> ExceptT CryptoError IO ByteString +encrypt (APublicEncryptKey _ k) = encrypt' k + +decrypt :: APrivateDecryptKey -> ByteString -> ExceptT CryptoError IO ByteString +decrypt (APrivateDecryptKey _ pk) = decrypt' pk -- | AEAD-GCM encryption. -- @@ -335,85 +834,115 @@ oaepParams = OAEP.defaultOAEPParams SHA256 -- | RSA OAEP encryption. -- -- Used as part of hybrid E2E encryption scheme and for SMP transport handshake. -encryptOAEP :: PublicKey -> ByteString -> ExceptT CryptoError IO ByteString -encryptOAEP (PublicKey k) aesKey = +encryptOAEP :: PublicKey RSA -> ByteString -> ExceptT CryptoError IO ByteString +encryptOAEP (PublicKeyRSA k) aesKey = liftEitherError RSAEncryptError $ OAEP.encrypt oaepParams k aesKey -- | RSA OAEP decryption. -- -- Used as part of hybrid E2E encryption scheme and for SMP transport handshake. -decryptOAEP :: PrivateKey -> ByteString -> ExceptT CryptoError IO ByteString -decryptOAEP pk encKey = +decryptOAEP :: PrivateKey RSA -> ByteString -> ExceptT CryptoError IO ByteString +decryptOAEP (PrivateKeyRSA pk) encKey = liftEitherError RSADecryptError $ - OAEP.decryptSafer oaepParams (rsaPrivateKey pk) encKey + OAEP.decryptSafer oaepParams pk encKey pssParams :: PSS.PSSParams SHA256 ByteString ByteString pssParams = PSS.defaultPSSParams SHA256 --- | RSA PSS message signing. +-- | Message signing. -- -- Used by SMP clients to sign SMP commands and by SMP agents to sign messages. -sign :: PrivateKey -> ByteString -> ExceptT CryptoError IO Signature -sign pk msg = ExceptT $ bimap RSASignError Signature <$> PSS.signSafer pssParams (rsaPrivateKey pk) msg +sign' :: SignatureAlgorithm a => PrivateKey a -> ByteString -> ExceptT CryptoError IO (Signature a) +sign' (PrivateKeyRSA pk) msg = ExceptT $ bimap RSASignError SignatureRSA <$> PSS.signSafer pssParams pk msg +sign' (PrivateKeyEd25519 pk k) msg = pure . SignatureEd25519 $ Ed25519.sign pk k msg +sign' (PrivateKeyEd448 pk k) msg = pure . SignatureEd448 $ Ed448.sign pk k msg --- | RSA PSS signature verification. +sign :: APrivateSignKey -> ByteString -> ExceptT CryptoError IO ASignature +sign (APrivateSignKey a k) = fmap (ASignature a) . sign' k + +-- | Signature verification. -- -- Used by SMP servers to authorize SMP commands and by SMP agents to verify messages. -verify :: PublicKey -> Signature -> ByteString -> Bool -verify (PublicKey k) (Signature sig) msg = PSS.verify pssParams k msg sig +verify' :: SignatureAlgorithm a => PublicKey a -> Signature a -> ByteString -> Bool +verify' (PublicKeyRSA k) (SignatureRSA sig) msg = PSS.verify pssParams k msg sig +verify' (PublicKeyEd25519 k) (SignatureEd25519 sig) msg = Ed25519.verify k msg sig +verify' (PublicKeyEd448 k) (SignatureEd448 sig) msg = Ed448.verify k msg sig --- | Base-64 X509 encoding of RSA public key. --- --- Used as part of SMP queue information (out-of-band message). -serializePubKey :: PublicKey -> ByteString -serializePubKey = ("rsa:" <>) . encode . encodePubKey +verify :: APublicVerifyKey -> ASignature -> ByteString -> Bool +verify (APublicVerifyKey a k) (ASignature a' sig) msg = case testEquality a a' of + Just Refl -> verify' k sig msg + _ -> False --- | Base-64 PKCS8 encoding of PSA private key. --- --- Not used as part of SMP protocols. -serializePrivKey :: PrivateKey -> ByteString -serializePrivKey = ("rsa:" <>) . encode . encodePrivKey +pubVerifyKey :: APublicKey -> Either String APublicVerifyKey +pubVerifyKey (APublicKey a k) = case signatureAlgorithm a of + Just Dict -> Right $ APublicVerifyKey a k + _ -> Left "key does not support signature algorithms" --- Base-64 X509 RSA public key parser. -pubKeyP :: Parser PublicKey -pubKeyP = decodePubKey <$?> ("rsa:" *> base64P) +pubEncryptKey :: APublicKey -> Either String APublicEncryptKey +pubEncryptKey (APublicKey a k) = case encryptionAlgorithm a of + Just Dict -> Right $ APublicEncryptKey a k + _ -> Left "key does not support encryption algorithms" --- Binary X509 RSA public key parser. -binaryPubKeyP :: Parser PublicKey -binaryPubKeyP = decodePubKey <$?> A.takeByteString +pubKey' :: forall a. AlgorithmI a => APublicKey -> Either String (PublicKey a) +pubKey' (APublicKey a k) = case testEquality a $ sAlgorithm @a of + Just Refl -> Right k + _ -> Left "bad key algorithm" --- Base-64 PKCS8 RSA private key parser. -privKeyP :: Parser PrivateKey -privKeyP = decodePrivKey <$?> ("rsa:" *> base64P) +privSignKey :: APrivateKey -> Either String APrivateSignKey +privSignKey (APrivateKey a k) = case signatureAlgorithm a of + Just Dict -> Right $ APrivateSignKey a k + _ -> Left "key does not support signature algorithms" --- Binary PKCS8 RSA private key parser. -binaryPrivKeyP :: Parser PrivateKey -binaryPrivKeyP = decodePrivKey <$?> A.takeByteString +privDecryptKey :: APrivateKey -> Either String APrivateDecryptKey +privDecryptKey (APrivateKey a k) = case encryptionAlgorithm a of + Just Dict -> Right $ APrivateDecryptKey a k + _ -> Left "key does not support encryption algorithms" --- Binary X509 encoding of 'PublicKey'. -encodePubKey :: PublicKey -> ByteString -encodePubKey = encodeKey . PubKeyRSA . rsaPublicKey +privKey' :: forall a. AlgorithmI a => APrivateKey -> Either String (PrivateKey a) +privKey' (APrivateKey a k) = case testEquality a $ sAlgorithm @a of + Just Refl -> Right k + _ -> Left "bad key algorithm" --- Binary PKCS8 encoding of 'PrivateKey'. -encodePrivKey :: PrivateKey -> ByteString -encodePrivKey = encodeKey . PrivKeyRSA . rsaPrivateKey +publicToX509 :: PublicKey a -> PubKey +publicToX509 = \case + PublicKeyRSA k -> PubKeyRSA k + PublicKeyEd25519 k -> PubKeyEd25519 k + PublicKeyEd448 k -> PubKeyEd448 k + PublicKeyX25519 k -> PubKeyX25519 k + PublicKeyX448 k -> PubKeyX448 k -encodeKey :: ASN1Object a => a -> ByteString -encodeKey k = toStrict . encodeASN1 DER $ toASN1 k [] +privateToX509 :: PrivateKey a -> PrivKey +privateToX509 = \case + PrivateKeyRSA k -> PrivKeyRSA k + PrivateKeyEd25519 k _ -> PrivKeyEd25519 k + PrivateKeyEd448 k _ -> PrivKeyEd448 k + PrivateKeyX25519 k -> PrivKeyX25519 k + PrivateKeyX448 k -> PrivKeyX448 k + +encodeASNKey :: ASN1Object a => a -> ByteString +encodeASNKey k = toStrict . encodeASN1 DER $ toASN1 k [] -- Decoding of binary X509 'PublicKey'. -decodePubKey :: ByteString -> Either String PublicKey +decodePubKey :: ByteString -> Either String APublicKey decodePubKey = decodeKey >=> \case - (PubKeyRSA k, []) -> Right $ PublicKey k + (PubKeyRSA k, []) -> Right . APublicKey SRSA $ PublicKeyRSA k + (PubKeyEd25519 k, []) -> Right . APublicKey SEd25519 $ PublicKeyEd25519 k + (PubKeyEd448 k, []) -> Right . APublicKey SEd448 $ PublicKeyEd448 k + (PubKeyX25519 k, []) -> Right . APublicKey SX25519 $ PublicKeyX25519 k + (PubKeyX448 k, []) -> Right . APublicKey SX448 $ PublicKeyX448 k r -> keyError r -- Decoding of binary PKCS8 'PrivateKey'. -decodePrivKey :: ByteString -> Either String PrivateKey +decodePrivKey :: ByteString -> Either String APrivateKey decodePrivKey = decodeKey >=> \case - (PrivKeyRSA pk, []) -> Right $ PrivateKey pk + (PrivKeyRSA pk, []) -> Right . APrivateKey SRSA $ PrivateKeyRSA pk + (PrivKeyEd25519 k, []) -> Right . APrivateKey SEd25519 . PrivateKeyEd25519 k $ Ed25519.toPublic k + (PrivKeyEd448 k, []) -> Right . APrivateKey SEd448 . PrivateKeyEd448 k $ Ed448.toPublic k + (PrivKeyX25519 k, []) -> Right . APrivateKey SX25519 $ PrivateKeyX25519 k + (PrivKeyX448 k, []) -> Right . APrivateKey SX448 $ PrivateKeyX448 k r -> keyError r decodeKey :: ASN1Object a => ByteString -> Either String (a, [ASN1]) @@ -421,5 +950,5 @@ decodeKey = fromASN1 <=< first show . decodeASN1 DER . fromStrict keyError :: (a, [ASN1]) -> Either String b keyError = \case - (_, []) -> Left "not RSA key" + (_, []) -> Left "unknown key algorithm" _ -> Left "more than one key" diff --git a/src/Simplex/Messaging/Protocol.hs b/src/Simplex/Messaging/Protocol.hs index d80f25ed5..f6d072aa2 100644 --- a/src/Simplex/Messaging/Protocol.hs +++ b/src/Simplex/Messaging/Protocol.hs @@ -34,6 +34,7 @@ module Simplex.Messaging.Protocol SignedTransmission, SignedTransmissionOrError, RawTransmission, + SentRawTransmission, SignedRawTransmission, CorrId (..), QueueId, @@ -76,6 +77,7 @@ import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.Functor (($>)) import Data.Kind +import Data.Maybe (isNothing) import Data.String import Data.Time.Clock import Data.Time.ISO8601 @@ -109,18 +111,21 @@ deriving instance Show Cmd type Transmission = (CorrId, QueueId, Cmd) -- | SMP transmission with signature. -type SignedTransmission = (C.Signature, Transmission) +type SignedTransmission = (Maybe C.ASignature, Transmission) type TransmissionOrError = (CorrId, QueueId, Either ErrorType Cmd) -- | signed parsed transmission, with parsing error. -type SignedTransmissionOrError = (C.Signature, TransmissionOrError) +type SignedTransmissionOrError = (Maybe C.ASignature, TransmissionOrError) -- | unparsed SMP transmission with signature. type RawTransmission = (ByteString, ByteString, ByteString, ByteString) --- | unparsed SMP transmission with signature. -type SignedRawTransmission = (C.Signature, ByteString) +-- | unparsed sent SMP transmission with signature. +type SignedRawTransmission = (Maybe C.ASignature, ByteString, ByteString, ByteString) + +-- | unparsed sent SMP transmission with signature. +type SentRawTransmission = (Maybe C.ASignature, ByteString) -- | SMP queue ID for the recipient. type RecipientId = QueueId @@ -177,24 +182,24 @@ instance IsString CorrId where -- | Recipient's private key used by the recipient to authorize (sign) SMP commands. -- -- Only used by SMP agent, kept here so its definition is close to respective public key. -type RecipientPrivateKey = C.PrivateKey +type RecipientPrivateKey = C.APrivateSignKey -- | Recipient's public key used by SMP server to verify authorization of SMP commands. -type RecipientPublicKey = C.PublicKey +type RecipientPublicKey = C.APublicVerifyKey -- | Sender's private key used by the recipient to authorize (sign) SMP commands. -- -- Only used by SMP agent, kept here so its definition is close to respective public key. -type SenderPrivateKey = C.PrivateKey +type SenderPrivateKey = C.APrivateSignKey -- | Sender's public key used by SMP server to verify authorization of SMP commands. -type SenderPublicKey = C.PublicKey +type SenderPublicKey = C.APublicVerifyKey -- | Private key used by push notifications server to authorize (sign) LSTN command. -type NotifierPrivateKey = C.PrivateKey +type NotifierPrivateKey = C.APrivateSignKey -- | Public key used by SMP server to verify authorization of LSTN command sent by push notifications server. -type NotifierPublicKey = C.PublicKey +type NotifierPublicKey = C.APublicVerifyKey -- | SMP message server ID. type MsgId = Encoded @@ -243,11 +248,11 @@ instance Arbitrary CommandError where arbitrary = genericArbitraryU -- | SMP transmission parser. transmissionP :: Parser RawTransmission transmissionP = do - signature <- segment + sig <- segment corrId <- segment queueId <- segment command <- A.takeByteString - return (signature, corrId, queueId, command) + return (sig, corrId, queueId, command) where segment = A.takeTill (== ' ') <* " " @@ -273,11 +278,11 @@ commandP = <|> "ERR " *> serverError <|> "PONG" $> Cmd SBroker PONG where - newCmd = Cmd SRecipient . NEW <$> C.pubKeyP + newCmd = Cmd SRecipient . NEW <$> C.strKeyP idsResp = Cmd SBroker <$> (IDS <$> (base64P <* A.space) <*> base64P) nIdsResp = Cmd SBroker . NID <$> base64P - keyCmd = Cmd SRecipient . KEY <$> C.pubKeyP - nKeyCmd = Cmd SRecipient . NKEY <$> C.pubKeyP + keyCmd = Cmd SRecipient . KEY <$> C.strKeyP + nKeyCmd = Cmd SRecipient . NKEY <$> C.strKeyP sendCmd = do size <- A.decimal <* A.space Cmd SSender . SEND <$> A.take size <* A.space @@ -297,9 +302,9 @@ parseCommand = parse (commandP <* " " <* A.takeByteString) $ CMD SYNTAX -- | Serialize SMP command. serializeCommand :: Cmd -> ByteString serializeCommand = \case - Cmd SRecipient (NEW rKey) -> "NEW " <> C.serializePubKey rKey - Cmd SRecipient (KEY sKey) -> "KEY " <> C.serializePubKey sKey - Cmd SRecipient (NKEY nKey) -> "NKEY " <> C.serializePubKey nKey + Cmd SRecipient (NEW rKey) -> "NEW " <> C.serializeKey rKey + Cmd SRecipient (KEY sKey) -> "KEY " <> C.serializeKey sKey + Cmd SRecipient (NKEY nKey) -> "NKEY " <> C.serializeKey nKey Cmd SRecipient SUB -> "SUB" Cmd SRecipient ACK -> "ACK" Cmd SRecipient OFF -> "OFF" @@ -328,9 +333,9 @@ serializeErrorType :: ErrorType -> ByteString serializeErrorType = bshow -- | Send signed SMP transmission to TCP transport. -tPut :: Transport c => THandle c -> SignedRawTransmission -> IO (Either TransportError ()) -tPut th (C.Signature sig, t) = - tPutEncrypted th $ encode sig <> " " <> t <> " " +tPut :: Transport c => THandle c -> SentRawTransmission -> IO (Either TransportError ()) +tPut th (sig, t) = + tPutEncrypted th $ C.serializeSignature sig <> " " <> t <> " " -- | Serialize SMP transmission. serializeTransmission :: Transmission -> ByteString @@ -362,26 +367,21 @@ tGet fromParty th = liftIO (tGetParse th) >>= decodeParseValidate where decodeParseValidate :: Either TransportError RawTransmission -> m SignedTransmissionOrError decodeParseValidate = \case - Right (signature, corrId, queueId, command) -> - let decodedTransmission = liftM2 (,corrId,,command) (validSig =<< decode signature) (decode queueId) + Right (sig, corrId, queueId, command) -> + let decodedTransmission = liftM2 (,corrId,,command) (C.decodeSignature =<< decode sig) (decode queueId) in either (const $ tError corrId) tParseValidate decodedTransmission Left _ -> tError "" - validSig :: ByteString -> Either String ByteString - validSig sig - | B.null sig || C.validKeySize (B.length sig) = Right sig - | otherwise = Left "invalid signature size" - tError :: ByteString -> m SignedTransmissionOrError - tError corrId = return (C.Signature "", (CorrId corrId, "", Left BLOCK)) + tError corrId = return (Nothing, (CorrId corrId, "", Left BLOCK)) - tParseValidate :: RawTransmission -> m SignedTransmissionOrError + tParseValidate :: SignedRawTransmission -> m SignedTransmissionOrError tParseValidate t@(sig, corrId, queueId, command) = do let cmd = parseCommand command >>= fromParty >>= tCredentials t - return (C.Signature sig, (CorrId corrId, queueId, cmd)) + return (sig, (CorrId corrId, queueId, cmd)) - tCredentials :: RawTransmission -> Cmd -> Either ErrorType Cmd - tCredentials (signature, _, queueId, _) cmd = case cmd of + tCredentials :: SignedRawTransmission -> Cmd -> Either ErrorType Cmd + tCredentials (sig, _, queueId, _) cmd = case cmd of -- IDS response must not have queue ID Cmd SBroker IDS {} -> Right cmd -- ERR response does not always have queue ID @@ -396,7 +396,7 @@ tGet fromParty th = liftIO (tGetParse th) >>= decodeParseValidate | otherwise -> Right cmd -- NEW must have signature but NOT queue ID Cmd SRecipient NEW {} - | B.null signature -> Left $ CMD NO_AUTH + | isNothing sig -> Left $ CMD NO_AUTH | not (B.null queueId) -> Left $ CMD HAS_AUTH | otherwise -> Right cmd -- SEND must have queue ID, signature is not always required @@ -405,9 +405,9 @@ tGet fromParty th = liftIO (tGetParse th) >>= decodeParseValidate | otherwise -> Right cmd -- PING must not have queue ID or signature Cmd SSender PING - | B.null queueId && B.null signature -> Right cmd + | isNothing sig && B.null queueId -> Right cmd | otherwise -> Left $ CMD HAS_AUTH -- other client commands must have both signature and queue ID Cmd _ _ - | B.null signature || B.null queueId -> Left $ CMD NO_AUTH + | isNothing sig || B.null queueId -> Left $ CMD NO_AUTH | otherwise -> Right cmd diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index 3d4cd634c..0f173d111 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -37,6 +37,7 @@ import Data.Functor (($>)) import qualified Data.Map.Strict as M import Data.Maybe (isNothing) import Data.Time.Clock +import Data.Type.Equality import Network.Socket (ServiceName) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Protocol @@ -142,59 +143,78 @@ receive h Client {rcvQ} = forever $ do send :: (Transport c, MonadUnliftIO m) => THandle c -> Client -> m () send h Client {sndQ} = forever $ do t <- atomically $ readTBQueue sndQ - liftIO $ tPut h ("", serializeTransmission t) + liftIO $ tPut h (Nothing, serializeTransmission t) mkResp :: CorrId -> QueueId -> Command 'Broker -> Transmission mkResp corrId queueId command = (corrId, queueId, Cmd SBroker command) verifyTransmission :: forall m. (MonadUnliftIO m, MonadReader Env m) => SignedTransmission -> m Transmission -verifyTransmission (sig, t@(corrId, queueId, cmd)) = do +verifyTransmission (sig_, t@(corrId, queueId, cmd)) = do (corrId,queueId,) <$> case cmd of Cmd SBroker _ -> return $ smpErr INTERNAL -- it can only be client command, because `fromClient` was used Cmd SRecipient (NEW k) -> pure $ verifySignature k Cmd SRecipient _ -> verifyCmd SRecipient $ verifySignature . recipientKey - Cmd SSender (SEND _) -> verifyCmd SSender $ verifyMaybe sig . senderKey + Cmd SSender (SEND _) -> verifyCmd SSender $ verifyMaybe . senderKey Cmd SSender PING -> return cmd - Cmd SNotifier NSUB -> verifyCmd SNotifier $ verifyMaybe sig . fmap snd . notifier + Cmd SNotifier NSUB -> verifyCmd SNotifier $ verifyMaybe . fmap snd . notifier where verifyCmd :: SParty p -> (QueueRec -> Cmd) -> m Cmd verifyCmd party f = do st <- asks queueStore q <- atomically $ getQueue st party queueId - pure $ either (const $ dummyVerify authErr) f q - verifyMaybe :: C.Signature -> Maybe SenderPublicKey -> Cmd - verifyMaybe "" = maybe cmd (const authErr) - verifyMaybe _ = maybe authErr verifySignature - verifySignature :: C.PublicKey -> Cmd - verifySignature key = if verify key then cmd else authErr - verify key - | C.publicKeySize key == sigLen = cryptoVerify key - | otherwise = dummyVerify False - cryptoVerify key = C.verify key sig (serializeTransmission t) + pure $ either (const $ dummyVerify_ sig_ authErr) f q + verifyMaybe :: Maybe C.APublicVerifyKey -> Cmd + verifyMaybe (Just k) = verifySignature k + verifyMaybe _ = maybe cmd (const authErr) sig_ + verifySignature :: C.APublicVerifyKey -> Cmd + verifySignature key = case sig_ of + Just s -> if verify key s then cmd else authErr + _ -> authErr + verify :: C.APublicVerifyKey -> C.ASignature -> Bool + verify (C.APublicVerifyKey a k) sig@(C.ASignature a' s) = + case (testEquality a a', C.signatureSize k == C.signatureSize s) of + (Just Refl, True) -> cryptoVerify k s + _ -> dummyVerify sig False + cryptoVerify :: C.SignatureAlgorithm a => C.PublicKey a -> C.Signature a -> Bool + cryptoVerify k s = C.verify' k s (serializeTransmission t) + dummyVerify_ :: Maybe C.ASignature -> a -> a + dummyVerify_ = \case + Just s -> dummyVerify s + _ -> id + dummyVerify :: C.ASignature -> a -> a + dummyVerify (C.ASignature _ s) = seq $ cryptoVerify (dummyPublicKey s) s smpErr = Cmd SBroker . ERR authErr = smpErr AUTH - dummyVerify :: a -> a - dummyVerify = seq $ - cryptoVerify $ case sigLen of - 128 -> dummyKey128 - 256 -> dummyKey256 - 384 -> dummyKey384 - 512 -> dummyKey512 - _ -> dummyKey256 - sigLen = B.length $ C.unSignature sig -- These dummy keys are used with `dummyVerify` function to mitigate timing attacks -- by having the same time of the response whether a queue exists or nor, for all valid key/signature sizes -dummyKey128 :: C.PublicKey +dummyPublicKey :: C.Signature a -> C.PublicKey a +dummyPublicKey = \case + C.SignatureRSA s' -> case B.length s' of + 128 -> dummyKey128 + 256 -> dummyKey256 + 384 -> dummyKey384 + 512 -> dummyKey512 + _ -> dummyKey256 + C.SignatureEd25519 _ -> dummyKeyEd25519 + C.SignatureEd448 _ -> dummyKeyEd448 + +dummyKeyEd25519 :: C.PublicKey 'C.Ed25519 +dummyKeyEd25519 = "MCowBQYDK2VwAyEA139Oqs4QgpqbAmB0o7rZf6T19ryl7E65k4AYe0kE3Qs=" + +dummyKeyEd448 :: C.PublicKey 'C.Ed448 +dummyKeyEd448 = "MEMwBQYDK2VxAzoA6ibQc9XpkSLtwrf7PLvp81qW/etiumckVFImCMRdftcG/XopbOSaq9qyLhrgJWKOLyNrQPNVvpMA" + +dummyKey128 :: C.PublicKey 'C.RSA dummyKey128 = "MIIBIDANBgkqhkiG9w0BAQEFAAOCAQ0AMIIBCAKBgQC2oeA7s4roXN5K2N6022I1/2CTeMKjWH0m00bSZWa4N8LDKeFcShh8YUxZea5giAveViTRNOOVLgcuXbKvR3u24szN04xP0+KnYUuUUIIoT3YSjX0IlomhDhhSyup4BmA0gAZ+D1OaIKZFX6J8yQ1Lr/JGLEfSRsBjw8l+4hs9OwKBgQDKA+YlZvGb3BcpDwKmatiCXN7ZRDWkjXbj8VAW5zV95tSRCCVN48hrFM1H4Ju2QMMUc6kPUVX+eW4ZjdCl5blIqIHMcTmsdcmsDDCg3PjUNrwc6bv/1TcirbAKcmnKt9iurIt6eerxSO7TZUXXMUVsi7eRwb/RUNhpCrpJ/hpIOw==" -dummyKey256 :: C.PublicKey +dummyKey256 :: C.PublicKey 'C.RSA dummyKey256 = "MIIBoDANBgkqhkiG9w0BAQEFAAOCAY0AMIIBiAKCAQEAxwmTvaqmdTbkfUGNi8Yu0L/T4cxuOlQlx3zGZ9X9Qx0+oZjknWK+QHrdWTcpS+zH4Hi7fP6kanOQoQ90Hj6Ghl57VU1GEdUPywSw4i1/7t0Wv9uT9Q2ktHp2rqVo3xkC9IVIpL7EZAxdRviIN2OsOB3g4a/F1ZpjxcAaZeOMUugiAX1+GtkLuE0Xn4neYjCaOghLxQTdhybN70VtnkiQLx/X9NjkDIl/spYGm3tQFMyYKkP6IWoEpj0926hJ0fmlmhy8tAOhlZsb/baW5cgkEZ3E9jVVrySCgQzoLQgma610FIISRpRJbSyv26jU7MkMxiyuBiDaFOORkXFttoKbtQKBgEbDS9II2brsz+vfI7uP8atFcawkE52cx4M1UWQhqb1H3tBiRl+qO+dMq1pPQF2bW7dlZAWYzS4W/367bTAuALHBDGB8xi1P4Njhh9vaOgTvuqrHG9NJQ85BLy0qGw8rjIWSIXVmVpfrXFJ8po5l04UE258Ll2yocv3QRQmddQW9" -dummyKey384 :: C.PublicKey +dummyKey384 :: C.PublicKey 'C.RSA dummyKey384 = "MIICITANBgkqhkiG9w0BAQEFAAOCAg4AMIICCQKCAYEAthExp77lSFBMB0RedjgKIU+oNH5lMGdMqDCG0E5Ly7X49rFpfDMMN08GDIgvzg9kcwV3ScbPcjUE19wmAShX9f9k3w38KM3wmIBKSiuCREQl0V3xAYp1SYwiAkMNSSwxuIkDEeSOR56WdEcZvqbB4lY9MQlUv70KriPDxZaqKCTKslUezXHQuYPQX6eMnGFK7hxz5Kl5MajV52d+5iXsa8CA+m/e1KVnbelCO+xhN89xG8ALt0CJ9k5Wwo3myLgXi4dmNankCmg8jkh+7y2ywkzxMwH1JydDtV/FLzkbZsbPR2w93TNrTq1RJOuqMyh0VtdBSpxNW/Ft988TkkX2BAWzx82INw7W6/QbHGNtHNB995R4sgeYy8QbEpNGBhQnfQh7yRWygLTVXWKApQzzfCeIoDDWUS7dMv/zXoasAnpDBj+6UhHv3BHrps7kBvRyZQ2d/nUuAqiGd43ljJ++n6vNyFLgZoiV7HLia/FOGMkdt7j92CNmFHxiT6Xl7kRHAoGBAPNoWny2O7LBxzAKMLmQVHBAiKp6RMx+7URvtQDHDHPaZ7F3MvtvmYWwGzund3cQFAaV1EkJoYeI3YRuj6xdXgMyMaP54On++btArb6jUtZuvlC98qE8dEEHQNh+7TsCiMU+ivbeKFxS9A/B7OVedoMnPoJWhatbA9zB/6L1GNPh" -dummyKey512 :: C.PublicKey +dummyKey512 :: C.PublicKey 'C.RSA dummyKey512 = "MIICoDANBgkqhkiG9w0BAQEFAAOCAo0AMIICiAKCAgEArkCY9DuverJ4mmzDektv9aZMFyeRV46WZK9NsOBKEc+1ncqMs+LhLti9asKNgUBRbNzmbOe0NYYftrUpwnATaenggkTFxxbJ4JGJuGYbsEdFWkXSvrbWGtM8YUmn5RkAGme12xQ89bSM4VoJAGnrYPHwmcQd+KYCPZvTUsxaxgrJTX65ejHN9BsAn8XtGViOtHTDJO9yUMD2WrJvd7wnNa+0ugEteDLzMU++xS98VC+uA1vfauUqi3yXVchdfrLdVUuM+JE0gUEXCgzjuHkaoHiaGNiGhdPYoAJJdOKQOIHAKdk7Th6OPhirPhc9XYNB4O8JDthKhNtfokvFIFlC4QBRzJhpLIENaEBDt08WmgpOnecZB/CuxkqqOrNa8j5K5jNrtXAI67W46VEC2jeQy/gZwb64Zit2A4D00xXzGbQTPGj4ehcEMhLx5LSCygViEf0w0tN3c3TEyUcgPzvECd2ZVpQLr9Z4a07Ebr+YSuxcHhjg4Rg1VyJyOTTvaCBGm5X2B3+tI4NUttmikIHOYpBnsLmHY2BgfH2KcrIsDyAhInXmTFr/L2+erFarUnlfATd2L8Ti43TNHDedO6k6jI5Gyi62yPwjqPLEIIK8l+pIeNfHJ3pPmjhHBfzFcQLMMMXffHWNK8kWklrQXK+4j4HiPcTBvlO1FEtG9nEIZhUCgYA4a6WtI2k5YNli1C89GY5rGUY7RP71T6RWri/D3Lz9T7GvU+FemAyYmsvCQwqijUOur0uLvwSP8VdxpSUcrjJJSWur2hrPWzWlu0XbNaeizxpFeKbQP+zSrWJ1z8RwfAeUjShxt8q1TuqGqY10wQyp3nyiTGvS+KwZVj5h5qx8NQ==" client :: forall m. (MonadUnliftIO m, MonadReader Env m) => Client -> Server -> m () @@ -266,10 +286,10 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ = sndQ'} Server withLog $ \s -> logAddNotifier s queueId nId nKey pure $ NID nId - checkKeySize :: Monad m' => C.PublicKey -> m' (Command 'Broker) -> m' Transmission + checkKeySize :: Monad m' => C.APublicVerifyKey -> m' (Command 'Broker) -> m' Transmission checkKeySize key action = mkResp corrId queueId - <$> if C.validKeySize $ C.publicKeySize key + <$> if C.validKeySize key then action else pure . ERR $ CMD KEY_SIZE diff --git a/src/Simplex/Messaging/Server/Env/STM.hs b/src/Simplex/Messaging/Server/Env/STM.hs index 83f2f2633..f414c71e3 100644 --- a/src/Simplex/Messaging/Server/Env/STM.hs +++ b/src/Simplex/Messaging/Server/Env/STM.hs @@ -30,7 +30,7 @@ data ServerConfig = ServerConfig msgIdBytes :: Int, storeLog :: Maybe (StoreLog 'ReadMode), blockSize :: Int, - serverPrivateKey :: C.PrivateKey + serverPrivateKey :: C.PrivateKey 'C.RSA -- serverId :: ByteString } @@ -40,7 +40,7 @@ data Env = Env queueStore :: QueueStore, msgStore :: STMMsgStore, idsDrg :: TVar ChaChaDRG, - serverKeyPair :: C.KeyPair, + serverKeyPair :: C.KeyPair 'C.RSA, storeLog :: Maybe (StoreLog 'WriteMode) } @@ -94,7 +94,7 @@ newEnv config = do idsDrg <- drgNew >>= newTVarIO s' <- restoreQueues queueStore `mapM` storeLog (config :: ServerConfig) let pk = serverPrivateKey config - serverKeyPair = (C.publicKey' pk, pk) + serverKeyPair = (C.publicKey pk, pk) return Env {config, server, queueStore, msgStore, idsDrg, serverKeyPair, storeLog = s'} where restoreQueues :: QueueStore -> StoreLog 'ReadMode -> m (StoreLog 'WriteMode) diff --git a/src/Simplex/Messaging/Server/StoreLog.hs b/src/Simplex/Messaging/Server/StoreLog.hs index 9f7fb5552..2a0d23929 100644 --- a/src/Simplex/Messaging/Server/StoreLog.hs +++ b/src/Simplex/Messaging/Server/StoreLog.hs @@ -62,33 +62,33 @@ storeLogRecordP = <|> "DELETE " *> (DeleteQueue <$> base64P) where createQueueP = CreateQueue <$> queueRecP - secureQueueP = SecureQueue <$> base64P <* A.space <*> C.pubKeyP + secureQueueP = SecureQueue <$> base64P <* A.space <*> C.strKeyP addNotifierP = - AddNotifier <$> base64P <* A.space <*> base64P <* A.space <*> C.pubKeyP + AddNotifier <$> base64P <* A.space <*> base64P <* A.space <*> C.strKeyP queueRecP = do recipientId <- "rid=" *> base64P <* A.space senderId <- "sid=" *> base64P <* A.space - recipientKey <- "rk=" *> C.pubKeyP <* A.space - senderKey <- "sk=" *> optional C.pubKeyP - notifier <- optional $ (,) <$> (" nid=" *> base64P) <*> (" nk=" *> C.pubKeyP) + recipientKey <- "rk=" *> C.strKeyP <* A.space + senderKey <- "sk=" *> optional C.strKeyP + notifier <- optional $ (,) <$> (" nid=" *> base64P) <*> (" nk=" *> C.strKeyP) pure QueueRec {recipientId, senderId, recipientKey, senderKey, notifier, status = QueueActive} serializeStoreLogRecord :: StoreLogRecord -> ByteString serializeStoreLogRecord = \case CreateQueue q -> "CREATE " <> serializeQueue q - SecureQueue rId sKey -> "SECURE " <> encode rId <> " " <> C.serializePubKey sKey - AddNotifier rId nId nKey -> B.unwords ["NOTIFIER", encode rId, encode nId, C.serializePubKey nKey] + SecureQueue rId sKey -> "SECURE " <> encode rId <> " " <> C.serializeKey sKey + AddNotifier rId nId nKey -> B.unwords ["NOTIFIER", encode rId, encode nId, C.serializeKey nKey] DeleteQueue rId -> "DELETE " <> encode rId where serializeQueue QueueRec {recipientId, senderId, recipientKey, senderKey, notifier} = B.unwords [ "rid=" <> encode recipientId, "sid=" <> encode senderId, - "rk=" <> C.serializePubKey recipientKey, - "sk=" <> maybe "" C.serializePubKey senderKey + "rk=" <> C.serializeKey recipientKey, + "sk=" <> maybe "" C.serializeKey senderKey ] <> maybe "" serializeNotifier notifier - serializeNotifier (nId, nKey) = " nid=" <> encode nId <> " nk=" <> C.serializePubKey nKey + serializeNotifier (nId, nKey) = " nid=" <> encode nId <> " nk=" <> C.serializeKey nKey openWriteStoreLog :: FilePath -> IO (StoreLog 'WriteMode) openWriteStoreLog f = WriteStoreLog f <$> openFile f WriteMode diff --git a/src/Simplex/Messaging/Transport.hs b/src/Simplex/Messaging/Transport.hs index 6c15576b6..603045fbf 100644 --- a/src/Simplex/Messaging/Transport.hs +++ b/src/Simplex/Messaging/Transport.hs @@ -1,5 +1,6 @@ {-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE BlockArguments #-} +{-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DuplicateRecordFields #-} @@ -345,7 +346,7 @@ makeNextIV SessionKey {baseIV, counter} = atomically $ do -- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#appendix-a -- -- The numbers in function names refer to the steps in the document. -serverHandshake :: forall c. Transport c => c -> Int -> C.KeyPair -> ExceptT TransportError IO (THandle c) +serverHandshake :: forall c. Transport c => c -> Int -> C.KeyPair 'C.RSA -> ExceptT TransportError IO (THandle c) serverHandshake c srvBlockSize (k, pk) = do checkValidBlockSize srvBlockSize liftIO sendHeaderAndPublicKey_1 @@ -358,13 +359,13 @@ serverHandshake c srvBlockSize (k, pk) = do where sendHeaderAndPublicKey_1 :: IO () sendHeaderAndPublicKey_1 = do - let sKey = C.encodePubKey k + let sKey = C.encodeKey k header = ServerHeader {blockSize = srvBlockSize, keySize = B.length sKey} cPut c $ binaryServerHeader header cPut c sKey receiveEncryptedKeys_4 :: ExceptT TransportError IO ByteString receiveEncryptedKeys_4 = - liftIO (cGet c $ C.publicKeySize k) >>= \case + liftIO (cGet c $ C.keySize k) >>= \case "" -> throwE $ TEHandshake TERMINATED ks -> pure ks decryptParseKeys_5 :: ByteString -> ExceptT TransportError IO ClientHandshake @@ -390,7 +391,7 @@ clientHandshake c blkSize_ keyHash = do getWelcome_6 th >>= checkVersion pure th where - getHeaderAndPublicKey_1_2 :: ExceptT TransportError IO (C.PublicKey, Int) + getHeaderAndPublicKey_1_2 :: ExceptT TransportError IO (C.PublicKey 'C.RSA, Int) getHeaderAndPublicKey_1_2 = do header <- liftIO (cGet c serverHeaderSize) ServerHeader {blockSize, keySize} <- liftEither $ parse serverHeaderP (TEHandshake HEADER) header @@ -399,8 +400,8 @@ clientHandshake c blkSize_ keyHash = do maybe (pure ()) (validateKeyHash_2 s) keyHash key <- liftEither $ parseKey s pure (key, blockSize) - parseKey :: ByteString -> Either TransportError C.PublicKey - parseKey = first (const $ TEHandshake RSA_KEY) . parseAll C.binaryPubKeyP + parseKey :: ByteString -> Either TransportError (C.PublicKey 'C.RSA) + parseKey = first (const $ TEHandshake RSA_KEY) . parseAll C.binaryKeyP validateKeyHash_2 :: ByteString -> C.KeyHash -> ExceptT TransportError IO () validateKeyHash_2 k (C.KeyHash kHash) | C.sha256Hash k == kHash = pure () @@ -412,7 +413,7 @@ clientHandshake c blkSize_ keyHash = do aesKey <- C.randomAesKey baseIV <- C.randomIV pure SessionKey {aesKey, baseIV, counter = undefined} - sendEncryptedKeys_4 :: C.PublicKey -> ClientHandshake -> ExceptT TransportError IO () + sendEncryptedKeys_4 :: C.PublicKey 'C.RSA -> ClientHandshake -> ExceptT TransportError IO () sendEncryptedKeys_4 k chs = liftError (const $ TEHandshake ENCRYPT) (C.encryptOAEP k $ serializeClientHandshake chs) >>= liftIO . cPut c diff --git a/tests/AgentTests/SQLiteTests.hs b/tests/AgentTests/SQLiteTests.hs index 83333fea5..d265b5d80 100644 --- a/tests/AgentTests/SQLiteTests.hs +++ b/tests/AgentTests/SQLiteTests.hs @@ -1,4 +1,5 @@ {-# LANGUAGE BlockArguments #-} +{-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} @@ -149,9 +150,18 @@ testForeignKeysEnabled = cData1 :: ConnData cData1 = ConnData {connId = "conn1"} -testPrivateKey :: C.PrivateKey +testPrivateSignKey :: C.APrivateSignKey +testPrivateSignKey = C.APrivateSignKey C.SRSA testPrivateKey + +testPrivateDecryptKey :: C.APrivateDecryptKey +testPrivateDecryptKey = C.APrivateDecryptKey C.SRSA testPrivateKey + +testPublicEncryptKey :: C.APublicEncryptKey +testPublicEncryptKey = C.APublicEncryptKey C.SRSA $ C.PublicKeyRSA $ R.PublicKey 1 2 3 + +testPrivateKey :: C.PrivateKey 'C.RSA testPrivateKey = - C.PrivateKey + C.PrivateKeyRSA R.PrivateKey { private_pub = R.PublicKey @@ -172,9 +182,9 @@ rcvQueue1 = RcvQueue { server = SMPServer "smp.simplex.im" (Just "5223") testKeyHash, rcvId = "1234", - rcvPrivateKey = testPrivateKey, + rcvPrivateKey = testPrivateSignKey, sndId = Just "2345", - decryptKey = testPrivateKey, + decryptKey = testPrivateDecryptKey, verifyKey = Nothing, status = New } @@ -184,9 +194,9 @@ sndQueue1 = SndQueue { server = SMPServer "smp.simplex.im" (Just "5223") testKeyHash, sndId = "3456", - sndPrivateKey = testPrivateKey, - encryptKey = C.PublicKey $ R.PublicKey 1 2 3, - signKey = testPrivateKey, + sndPrivateKey = testPrivateSignKey, + encryptKey = testPublicEncryptKey, + signKey = testPrivateSignKey, status = New } @@ -324,9 +334,9 @@ testUpgradeRcvConnToDuplex = SndQueue { server = SMPServer "smp.simplex.im" (Just "5223") testKeyHash, sndId = "2345", - sndPrivateKey = testPrivateKey, - encryptKey = C.PublicKey $ R.PublicKey 1 2 3, - signKey = testPrivateKey, + sndPrivateKey = testPrivateSignKey, + encryptKey = testPublicEncryptKey, + signKey = testPrivateSignKey, status = New } upgradeRcvConnToDuplex store "conn1" anotherSndQueue @@ -344,9 +354,9 @@ testUpgradeSndConnToDuplex = RcvQueue { server = SMPServer "smp.simplex.im" (Just "5223") testKeyHash, rcvId = "3456", - rcvPrivateKey = testPrivateKey, + rcvPrivateKey = testPrivateSignKey, sndId = Just "4567", - decryptKey = testPrivateKey, + decryptKey = testPrivateDecryptKey, verifyKey = Nothing, status = New } diff --git a/tests/SMPClient.hs b/tests/SMPClient.hs index 6892baaea..fdd88725b 100644 --- a/tests/SMPClient.hs +++ b/tests/SMPClient.hs @@ -13,6 +13,7 @@ import Control.Monad.Except (runExceptT) import Control.Monad.IO.Unlift import Crypto.Random import Data.ByteString.Base64 (encode) +import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Network.Socket import qualified Simplex.Messaging.Crypto as C @@ -36,7 +37,7 @@ testPort = "5000" testPort2 :: ServiceName testPort2 = "5001" -testKeyHashStr :: B.ByteString +testKeyHashStr :: ByteString testKeyHashStr = "KXNE1m2E1m0lm92WGKet9CL6+lO742Vy5G6nsrkvgs8=" testBlockSize :: Maybe Int @@ -140,8 +141,8 @@ runSmpTestN nClients test = withSmpServer (transport @c) $ run nClients [] run 0 hs = test hs run n hs = testSMPClient $ \h -> run (n - 1) (h : hs) -smpServerTest :: forall c. Transport c => TProxy c -> RawTransmission -> IO RawTransmission -smpServerTest _ cmd = runSmpTest $ \(h :: THandle c) -> tPutRaw h cmd >> tGetRaw h +smpServerTest :: forall c. Transport c => TProxy c -> SignedRawTransmission -> IO SignedRawTransmission +smpServerTest _ t = runSmpTest $ \(h :: THandle c) -> tPutRaw h t >> tGetRaw h smpTest :: Transport c => TProxy c -> (THandle c -> IO ()) -> Expectation smpTest _ test' = runSmpTest test' `shouldReturn` () @@ -167,12 +168,12 @@ smpTest4 _ test' = smpTestN 4 _test _test [h1, h2, h3, h4] = test' h1 h2 h3 h4 _test _ = error "expected 4 handles" -tPutRaw :: Transport c => THandle c -> RawTransmission -> IO () +tPutRaw :: Transport c => THandle c -> SignedRawTransmission -> IO () tPutRaw h (sig, corrId, queueId, command) = do let t = B.intercalate " " [corrId, queueId, command] - void $ tPut h (C.Signature sig, t) + void $ tPut h (sig, t) -tGetRaw :: Transport c => THandle c -> IO RawTransmission +tGetRaw :: Transport c => THandle c -> IO SignedRawTransmission tGetRaw h = do - ("", (CorrId corrId, qId, Right cmd)) <- tGet fromServer h - pure ("", corrId, encode qId, serializeCommand cmd) + (Nothing, (CorrId corrId, qId, Right cmd)) <- tGet fromServer h + pure (Nothing, corrId, encode qId, serializeCommand cmd) diff --git a/tests/ServerTests.hs b/tests/ServerTests.hs index 7c12b9e18..922636b36 100644 --- a/tests/ServerTests.hs +++ b/tests/ServerTests.hs @@ -44,14 +44,14 @@ serverTests t = do pattern Resp :: CorrId -> QueueId -> Command 'Broker -> SignedTransmissionOrError pattern Resp corrId queueId command <- ("", (corrId, queueId, Right (Cmd SBroker command))) -sendRecv :: Transport c => THandle c -> (ByteString, ByteString, ByteString, ByteString) -> IO SignedTransmissionOrError +sendRecv :: Transport c => THandle c -> (Maybe C.ASignature, ByteString, ByteString, ByteString) -> IO SignedTransmissionOrError sendRecv h (sgn, corrId, qId, cmd) = tPutRaw h (sgn, corrId, encode qId, cmd) >> tGet fromServer h -signSendRecv :: Transport c => THandle c -> C.PrivateKey -> (ByteString, ByteString, ByteString) -> IO SignedTransmissionOrError +signSendRecv :: Transport c => THandle c -> C.APrivateSignKey -> (ByteString, ByteString, ByteString) -> IO SignedTransmissionOrError signSendRecv h pk (corrId, qId, cmd) = do let t = B.intercalate " " [corrId, encode qId, cmd] Right sig <- runExceptT $ C.sign pk t - _ <- tPut h (sig, t) + _ <- tPut h (Just sig, t) tGet fromServer h cmdSEND :: ByteString -> ByteString @@ -64,8 +64,8 @@ testCreateSecure :: ATransport -> Spec testCreateSecure (ATransport t) = it "should create (NEW) and secure (KEY) queue" $ smpTest t $ \h -> do - (rPub, rKey) <- C.generateKeyPair rsaKeySize - Resp "abcd" rId1 (IDS rId sId) <- signSendRecv h rKey ("abcd", "", "NEW " <> C.serializePubKey rPub) + (rPub, rKey) <- C.generateSignatureKeyPair rsaKeySize C.SRSA + Resp "abcd" rId1 (IDS rId sId) <- signSendRecv h rKey ("abcd", "", "NEW " <> C.serializeKey rPub) (rId1, "") #== "creates queue" Resp "bcda" sId1 ok1 <- sendRecv h ("", "bcda", sId, "SEND 5 hello ") @@ -81,12 +81,12 @@ testCreateSecure (ATransport t) = Resp "dabc" _ err6 <- signSendRecv h rKey ("dabc", rId, "ACK") (err6, ERR NO_MSG) #== "replies ERR when message acknowledged without messages" - (sPub, sKey) <- C.generateKeyPair rsaKeySize + (sPub, sKey) <- C.generateSignatureKeyPair rsaKeySize C.SRSA Resp "abcd" sId2 err1 <- signSendRecv h sKey ("abcd", sId, "SEND 5 hello ") (err1, ERR AUTH) #== "rejects signed SEND" (sId2, sId) #== "same queue ID in response 2" - let keyCmd = "KEY " <> C.serializePubKey sPub + let keyCmd = "KEY " <> C.serializeKey sPub Resp "bcda" _ err2 <- sendRecv h (sampleSig, "bcda", rId, keyCmd) (err2, ERR AUTH) #== "rejects KEY with wrong signature" @@ -116,12 +116,12 @@ testCreateDelete :: ATransport -> Spec testCreateDelete (ATransport t) = it "should create (NEW), suspend (OFF) and delete (DEL) queue" $ smpTest2 t $ \rh sh -> do - (rPub, rKey) <- C.generateKeyPair rsaKeySize - Resp "abcd" rId1 (IDS rId sId) <- signSendRecv rh rKey ("abcd", "", "NEW " <> C.serializePubKey rPub) + (rPub, rKey) <- C.generateSignatureKeyPair rsaKeySize C.SRSA + Resp "abcd" rId1 (IDS rId sId) <- signSendRecv rh rKey ("abcd", "", "NEW " <> C.serializeKey rPub) (rId1, "") #== "creates queue" - (sPub, sKey) <- C.generateKeyPair rsaKeySize - Resp "bcda" _ ok1 <- signSendRecv rh rKey ("bcda", rId, "KEY " <> C.serializePubKey sPub) + (sPub, sKey) <- C.generateSignatureKeyPair rsaKeySize C.SRSA + Resp "bcda" _ ok1 <- signSendRecv rh rKey ("bcda", rId, "KEY " <> C.serializeKey sPub) (ok1, OK) #== "secures queue" Resp "cdab" _ ok2 <- signSendRecv sh sKey ("cdab", sId, "SEND 5 hello ") @@ -184,22 +184,22 @@ testDuplex :: ATransport -> Spec testDuplex (ATransport t) = it "should create 2 simplex connections and exchange messages" $ smpTest2 t $ \alice bob -> do - (arPub, arKey) <- C.generateKeyPair rsaKeySize - Resp "abcd" _ (IDS aRcv aSnd) <- signSendRecv alice arKey ("abcd", "", "NEW " <> C.serializePubKey arPub) + (arPub, arKey) <- C.generateSignatureKeyPair rsaKeySize C.SRSA + Resp "abcd" _ (IDS aRcv aSnd) <- signSendRecv alice arKey ("abcd", "", "NEW " <> C.serializeKey arPub) -- aSnd ID is passed to Bob out-of-band - (bsPub, bsKey) <- C.generateKeyPair rsaKeySize - Resp "bcda" _ OK <- sendRecv bob ("", "bcda", aSnd, cmdSEND $ "key " <> C.serializePubKey bsPub) + (bsPub, bsKey) <- C.generateSignatureKeyPair rsaKeySize C.SRSA + Resp "bcda" _ OK <- sendRecv bob ("", "bcda", aSnd, cmdSEND $ "key " <> C.serializeKey bsPub) -- "key ..." is ad-hoc, different from SMP protocol Resp "" _ (MSG _ _ msg1) <- tGet fromServer alice Resp "cdab" _ OK <- signSendRecv alice arKey ("cdab", aRcv, "ACK") ["key", bobKey] <- return $ B.words msg1 - (bobKey, C.serializePubKey bsPub) #== "key received from Bob" + (bobKey, C.serializeKey bsPub) #== "key received from Bob" Resp "dabc" _ OK <- signSendRecv alice arKey ("dabc", aRcv, "KEY " <> bobKey) - (brPub, brKey) <- C.generateKeyPair rsaKeySize - Resp "abcd" _ (IDS bRcv bSnd) <- signSendRecv bob brKey ("abcd", "", "NEW " <> C.serializePubKey brPub) + (brPub, brKey) <- C.generateSignatureKeyPair rsaKeySize C.SRSA + Resp "abcd" _ (IDS bRcv bSnd) <- signSendRecv bob brKey ("abcd", "", "NEW " <> C.serializeKey brPub) Resp "bcda" _ OK <- signSendRecv bob bsKey ("bcda", aSnd, cmdSEND $ "reply_id " <> encode bSnd) -- "reply_id ..." is ad-hoc, it is not a part of SMP protocol @@ -208,14 +208,14 @@ testDuplex (ATransport t) = ["reply_id", bId] <- return $ B.words msg2 (bId, encode bSnd) #== "reply queue ID received from Bob" - (asPub, asKey) <- C.generateKeyPair rsaKeySize - Resp "dabc" _ OK <- sendRecv alice ("", "dabc", bSnd, cmdSEND $ "key " <> C.serializePubKey asPub) + (asPub, asKey) <- C.generateSignatureKeyPair rsaKeySize C.SRSA + Resp "dabc" _ OK <- sendRecv alice ("", "dabc", bSnd, cmdSEND $ "key " <> C.serializeKey asPub) -- "key ..." is ad-hoc, different from SMP protocol Resp "" _ (MSG _ _ msg3) <- tGet fromServer bob Resp "abcd" _ OK <- signSendRecv bob brKey ("abcd", bRcv, "ACK") ["key", aliceKey] <- return $ B.words msg3 - (aliceKey, C.serializePubKey asPub) #== "key received from Alice" + (aliceKey, C.serializeKey asPub) #== "key received from Alice" Resp "bcda" _ OK <- signSendRecv bob brKey ("bcda", bRcv, "KEY " <> aliceKey) Resp "cdab" _ OK <- signSendRecv bob bsKey ("cdab", aSnd, "SEND 8 hi alice ") @@ -234,8 +234,8 @@ testSwitchSub :: ATransport -> Spec testSwitchSub (ATransport t) = it "should create simplex connections and switch subscription to another TCP connection" $ smpTest3 t $ \rh1 rh2 sh -> do - (rPub, rKey) <- C.generateKeyPair rsaKeySize - Resp "abcd" _ (IDS rId sId) <- signSendRecv rh1 rKey ("abcd", "", "NEW " <> C.serializePubKey rPub) + (rPub, rKey) <- C.generateSignatureKeyPair rsaKeySize C.SRSA + Resp "abcd" _ (IDS rId sId) <- signSendRecv rh1 rKey ("abcd", "", "NEW " <> C.serializeKey rPub) Resp "bcda" _ ok1 <- sendRecv sh ("", "bcda", sId, "SEND 5 test1 ") (ok1, OK) #== "sent test message 1" Resp "cdab" _ ok2 <- sendRecv sh ("", "cdab", sId, cmdSEND "test2, no ACK") @@ -271,9 +271,9 @@ testSwitchSub (ATransport t) = testWithStoreLog :: ATransport -> Spec testWithStoreLog at@(ATransport t) = it "should store simplex queues to log and restore them after server restart" $ do - (sPub1, sKey1) <- C.generateKeyPair rsaKeySize - (sPub2, sKey2) <- C.generateKeyPair rsaKeySize - (nPub, nKey) <- C.generateKeyPair rsaKeySize + (sPub1, sKey1) <- C.generateSignatureKeyPair rsaKeySize C.SRSA + (sPub2, sKey2) <- C.generateSignatureKeyPair rsaKeySize C.SRSA + (nPub, nKey) <- C.generateSignatureKeyPair rsaKeySize C.SRSA senderId1 <- newTVarIO "" senderId2 <- newTVarIO "" notifierId <- newTVarIO "" @@ -281,7 +281,7 @@ testWithStoreLog at@(ATransport t) = withSmpServerStoreLogOn at testPort . runTest t $ \h -> runClient t $ \h1 -> do (sId1, rId, rKey) <- createAndSecureQueue h sPub1 atomically $ writeTVar senderId1 sId1 - Resp "abcd" _ (NID nId) <- signSendRecv h rKey ("abcd", rId, "NKEY " <> C.serializePubKey nPub) + Resp "abcd" _ (NID nId) <- signSendRecv h rKey ("abcd", rId, "NKEY " <> C.serializeKey nPub) atomically $ writeTVar notifierId nId Resp "dabc" _ OK <- signSendRecv h1 nKey ("dabc", nId, "NSUB") Resp "bcda" _ OK <- signSendRecv h sKey1 ("bcda", sId1, "SEND 5 hello ") @@ -332,11 +332,11 @@ testWithStoreLog at@(ATransport t) = Right l -> pure l Left (_ :: SomeException) -> logSize -createAndSecureQueue :: Transport c => THandle c -> SenderPublicKey -> IO (SenderId, RecipientId, C.PrivateKey) +createAndSecureQueue :: Transport c => THandle c -> SenderPublicKey -> IO (SenderId, RecipientId, C.APrivateSignKey) createAndSecureQueue h sPub = do - (rPub, rKey) <- C.generateKeyPair rsaKeySize - Resp "abcd" "" (IDS rId sId) <- signSendRecv h rKey ("abcd", "", "NEW " <> C.serializePubKey rPub) - let keyCmd = "KEY " <> C.serializePubKey sPub + (rPub, rKey) <- C.generateSignatureKeyPair rsaKeySize C.SRSA + Resp "abcd" "" (IDS rId sId) <- signSendRecv h rKey ("abcd", "", "NEW " <> C.serializeKey rPub) + let keyCmd = "KEY " <> C.serializeKey sPub Resp "dabc" rId' OK <- signSendRecv h rKey ("dabc", rId, keyCmd) (rId', rId) #== "same queue ID" pure (sId, rId, rKey) @@ -347,54 +347,81 @@ testTiming (ATransport t) = smpTest2 t $ \rh sh -> mapM_ (testSameTiming rh sh) - [ (128, 128, 100), - (128, 256, 25), - (128, 384, 15), - -- (128, 512, 15), - (256, 128, 100), - (256, 256, 25), - (256, 384, 15), - -- (256, 512, 15), - (384, 128, 100), - (384, 256, 25), - (384, 384, 15) - -- (384, 512, 15), - -- (512, 128, 100), - -- (512, 256, 25), + [ (32, 32, 200), + (32, 57, 100), + (32, 128, 40), + (32, 256, 20), + (57, 32, 200), + (57, 57, 100), + (57, 128, 40), + (57, 256, 20), + (128, 32, 200), + (128, 57, 100), + (128, 128, 40), + (128, 256, 20), + (256, 32, 200), + (256, 57, 100), + (256, 128, 40), + (256, 256, 20) + -- (256, 384, 15), + -- (256, 512, 10), + -- (384, 128, 40), + -- (384, 256, 20), + -- (384, 384, 15), + -- (384, 512, 10), + -- (512, 128, 40), + -- (512, 256, 20), -- (512, 384, 15), - -- (512, 512, 15) + -- (512, 512, 10) ] where timeRepeat n = fmap fst . timeItT . forM_ (replicate n ()) . const - similarTime t1 t2 = abs (t1 - t2) / t1 < 0.2 `shouldBe` True + similarTime t1 t2 = abs (t2 / t1 - 1) < 0.25 `shouldBe` True testSameTiming :: Transport c => THandle c -> THandle c -> (Int, Int, Int) -> Expectation - testSameTiming rh sh (senderKeySize, badKeySize, n) = do - (rPub, rKey) <- C.generateKeyPair rsaKeySize - Resp "abcd" "" (IDS rId sId) <- signSendRecv rh rKey ("abcd", "", "NEW " <> C.serializePubKey rPub) + testSameTiming rh sh (goodKeySize, badKeySize, n) = do + (rPub, rKey) <- generateKeys goodKeySize + Resp "abcd" "" (IDS rId sId) <- signSendRecv rh rKey ("abcd", "", "NEW " <> C.serializeKey rPub) + Resp "cdab" _ OK <- signSendRecv rh rKey ("cdab", rId, "SUB") - (sPub, sKey) <- C.generateKeyPair senderKeySize - let keyCmd = "KEY " <> C.serializePubKey sPub + (_, badKey) <- generateKeys badKeySize + -- runTimingTest rh badKey rId "SUB" + + (sPub, sKey) <- generateKeys goodKeySize + let keyCmd = "KEY " <> C.serializeKey sPub Resp "dabc" _ OK <- signSendRecv rh rKey ("dabc", rId, keyCmd) - (_, badKey) <- C.generateKeyPair badKeySize Resp "bcda" _ OK <- signSendRecv sh sKey ("bcda", sId, "SEND 5 hello ") - timeWrongKey <- timeRepeat n $ do - Resp "cdab" _ (ERR AUTH) <- signSendRecv sh badKey ("cdab", sId, "SEND 5 hello ") - return () - timeNoQueue <- timeRepeat n $ do - Resp "dabc" _ (ERR AUTH) <- signSendRecv sh badKey ("dabc", "1234", "SEND 5 hello ") - return () Resp "" _ (MSG _ _ "hello") <- tGet fromServer rh - similarTime timeNoQueue timeWrongKey + runTimingTest sh badKey sId "SEND 5 hello " + where + generateKeys = \case + 32 -> C.generateSignatureKeyPair 0 C.SEd25519 + 57 -> C.generateSignatureKeyPair 0 C.SEd448 + size -> C.generateSignatureKeyPair size C.SRSA + runTimingTest h badKey qId cmd = do + timeWrongKey <- timeRepeat n $ do + Resp "cdab" _ (ERR AUTH) <- signSendRecv h badKey ("cdab", qId, cmd) + return () + timeNoQueue <- timeRepeat n $ do + Resp "dabc" _ (ERR AUTH) <- signSendRecv h badKey ("dabc", "1234", cmd) + return () + -- (putStrLn . unwords . map show) + -- [ fromIntegral goodKeySize, + -- fromIntegral badKeySize, + -- timeWrongKey, + -- timeNoQueue, + -- timeWrongKey / timeNoQueue - 1 + -- ] + similarTime timeNoQueue timeWrongKey testMessageNotifications :: ATransport -> Spec testMessageNotifications (ATransport t) = it "should create simplex connection, subscribe notifier and deliver notifications" $ do - (sPub, sKey) <- C.generateKeyPair rsaKeySize - (nPub, nKey) <- C.generateKeyPair rsaKeySize + (sPub, sKey) <- C.generateSignatureKeyPair rsaKeySize C.SRSA + (nPub, nKey) <- C.generateSignatureKeyPair rsaKeySize C.SRSA smpTest4 t $ \rh sh nh1 nh2 -> do (sId, rId, rKey) <- createAndSecureQueue rh sPub - Resp "1" _ (NID nId) <- signSendRecv rh rKey ("1", rId, "NKEY " <> C.serializePubKey nPub) + Resp "1" _ (NID nId) <- signSendRecv rh rKey ("1", rId, "NKEY " <> C.serializeKey nPub) Resp "2" _ OK <- signSendRecv nh1 nKey ("2", nId, "NSUB") Resp "3" _ OK <- signSendRecv sh sKey ("3", sId, "SEND 5 hello ") Resp "" _ (MSG _ _ "hello") <- tGet fromServer rh @@ -412,8 +439,8 @@ testMessageNotifications (ATransport t) = samplePubKey :: ByteString samplePubKey = "rsa:MIIBoDANBgkqhkiG9w0BAQEFAAOCAY0AMIIBiAKCAQEAtn1NI2tPoOGSGfad0aUg0tJ0kG2nzrIPGLiz8wb3dQSJC9xkRHyzHhEE8Kmy2cM4q7rNZIlLcm4M7oXOTe7SC4x59bLQG9bteZPKqXu9wk41hNamV25PWQ4zIcIRmZKETVGbwN7jFMpH7wxLdI1zzMArAPKXCDCJ5ctWh4OWDI6OR6AcCtEj+toCI6N6pjxxn5VigJtwiKhxYpoUJSdNM60wVEDCSUrZYBAuDH8pOxPfP+Tm4sokaFDTIG3QJFzOjC+/9nW4MUjAOFll9PCp9kaEFHJ/YmOYKMWNOCCPvLS6lxA83i0UaardkNLNoFS5paWfTlroxRwOC2T6PwO2ywKBgDjtXcSED61zK1seocQMyGRINnlWdhceD669kIHju/f6kAayvYKW3/lbJNXCmyinAccBosO08/0sUxvtuniIo18kfYJE0UmP1ReCjhMP+O+yOmwZJini/QelJk/Pez8IIDDWnY1qYQsN/q7ocjakOYrpGG7mig6JMFpDJtD6istR" -sampleSig :: ByteString -sampleSig = "\128\207*\159eq\220i!\"\157\161\130\184\226\246\232_\\\170`\180\160\230sI\154\197\211\252\SUB\246\206ELL\t9K\ESC\196?\128\215%\222\148\NAK;9\155f\164\217e\242\156\CAN9\253\r\170\174'w\211\228?\205)\215\150\255\247z\DC115\DC1{\bn\145\rKD,K\230\202d8\233\167|7y\t_S\EM\248\EOT\216\172\167d\181\224)\137\ACKo\197j#c\217\243\228.\167\228\205\144\vr\134" +sampleSig :: Maybe C.ASignature +sampleSig = "gM8qn2Vx3GkhIp2hgrji9uhfXKpgtKDmc0maxdP8GvbORUxMCTlLG8Q/gNcl3pQVOzmbZqTZZfKcGDn9DaquJ3fT5D/NKdeW//d6ETE1EXsIbpENS0QsS+bKZDjpp3w3eQlfUxn4BNisp2S14CmJBm/FaiNj2fPkLqfkzZALcoY=" syntaxTests :: ATransport -> Spec syntaxTests (ATransport t) = do @@ -452,5 +479,5 @@ syntaxTests (ATransport t) = do it "wrong terminator" $ (sampleSig, "bcda", "12345678", cmd <> "=") >#> ("", "bcda", "12345678", "ERR CMD SYNTAX") it "no signature" $ ("", "cdab", "12345678", cmd) >#> ("", "cdab", "12345678", "ERR CMD NO_AUTH") it "no queue ID" $ (sampleSig, "dabc", "", cmd) >#> ("", "dabc", "", "ERR CMD NO_AUTH") - (>#>) :: RawTransmission -> RawTransmission -> Expectation + (>#>) :: SignedRawTransmission -> SignedRawTransmission -> Expectation command >#> response = smpServerTest t command `shouldReturn` response From db509d0311525d624e3b67b11f4b5116fac6e100 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 28 Nov 2021 21:01:34 +0000 Subject: [PATCH 08/20] update protocol docs (#209) * update protocol docs * more protocol corrections * update connection request syntax * Update protocol/agent-protocol.md Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com> * Update protocol/agent-protocol.md Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com> * Update protocol/agent-protocol.md Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com> * Update protocol/agent-protocol.md Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com> * Update protocol/simplex-messaging.md Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com> Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com> --- protocol/agent-protocol.md | 145 ++++++++++++++++++++++------------ protocol/simplex-messaging.md | 31 +++++--- 2 files changed, 114 insertions(+), 62 deletions(-) diff --git a/protocol/agent-protocol.md b/protocol/agent-protocol.md index 41c1297b4..00dbc873b 100644 --- a/protocol/agent-protocol.md +++ b/protocol/agent-protocol.md @@ -31,7 +31,7 @@ The purpose of SMP agent protocol is to define the syntax and the semantics of communications between the client and the agent that connects to [SMP](./simplex-messaging.md) servers. It provides: -- convenient protocol to create and manage bi-directional (duplex) connections between the users of SMP agents consisting of two (or more) separate unidirectional (simplex) SMP queues, abstracting away multiple steps required to establish bi-directional connections and any information about the servers location from the users of the protocol. +- protocol to create and manage bi-directional (duplex) connections between the users of SMP agents consisting of two (or more) separate unidirectional (simplex) SMP queues, abstracting away multiple steps required to establish bi-directional connections and any information about the servers location from the users of the agent protocol. - management of E2E encryption between SMP agents, generating ephemeral asymmetric keys for each connection. - SMP command authentication on SMP servers, generating ephemeral keys for each SMP queue. - TCP/TLS transport handshake with SMP servers. @@ -44,48 +44,48 @@ SMP agent protocol provides no encryption or security on the client side - it is ## SMP agent -SMP agent communicates via SMP servers using [simplex messaging protocol (SMP)](./simplex-messaging.md) with other SMP agents according to the commands received from its users. This protocol is a middle layer in SimpleX protocols (above SMP protocol but below any application level protocol) - it is intended to be used by client-side applications that need secure asynchronous bi-directional communication channels ("connections"). +SMP agents communicate with each other via SMP servers using [simplex messaging protocol (SMP)](./simplex-messaging.md) according to the commands received from its users. This protocol is a middle layer in SimpleX protocols (above SMP protocol but below any application level protocol) - it is intended to be used by client-side applications that need secure asynchronous bi-directional communication channels ("connections"). -The agent must have a persistent storage to manage the states of known connections and of the client-side information of SMP queues that each connection consists of, and also the buffer of the most recent sent and received messages. The number of the messages that should be stored is implementation specific, depending on the error management approach that the agent implements; at the very least the agent must store the hash and id of the last received and sent message. +The agent must have a persistent storage to manage the states of known connections and of the client-side information of SMP queues that each connection consists of, and also the buffer of the most recent sent and received messages. The number of the messages that should be stored is implementation specific, depending on the error management approach that the agent implements; at the very least the agent must store the hashes and IDs of the last received and sent messages. ## SMP servers management -SMP agent protocol commands do not contain SMP servers that the agent will use to establish the connections. The servers are part of the agent configuration and can be dynamically added and removed by the agent implementation: +SMP agent protocol commands do not contain the addresses of the SMP servers that the agent will use to create and use the connections (excluding the server address in queue URIs used in JOIN command). The list of the servers is a part of the agent configuration and can be dynamically changed by the agent implementation: - by the client applications via any API that is outside of scope of this protocol. -- by the agents themselves based on servers availability and latency. +- by the agents themselves based on availability and latency of the configured servers. ## SMP agent protocol components SMP agent protocol has 3 main parts: -- the syntax and semantics of messages that SMP agents exchange between each other in order to: - - negotiate establishing unidirectional (simplex) encrypted queues on SMP server(s) +- the syntax and semantics of the messages that SMP agents exchange with each other in order to: + - negotiate establishing unidirectional (simplex) encrypted queues on SMP servers. - exchange client messages and delivery notifications, providing sequential message IDs and message integrity (by including the hash of the previous message). -- the syntax and semantics of the commands that are sent over TCP or other sequential protocol by agent clients to the agents. This protocol allows to create and manage multiple connections, each consisting of two or more SMP queues. -- the syntax and semantics of the message that the clients of SMP agents should send out-of-band (as pre-shared "invitation" including SMP server, queue ID and encryption key) to ensure [E2E encryption][1] the integrity of SMP queues and protection against active attacks ([MITM attacks][2]). +- the syntax and semantics of the commands that are sent by the agent clients to the agents. This protocol allows to create and manage multiple connections, each consisting of two or more SMP queues. +- the syntax and semantics of the message that the clients of SMP agents should send out-of-band (as pre-shared "invitation" including queue URIs) to protect [E2E encryption][1] from active attacks ([MITM attacks][2]). ## Duplex connection procedure ![Duplex connection procedure](./diagrams/duplex-messaging/duplex-creating.svg) -The procedure of establishing a duplex connection is explained on the example of Alice and Bob creating a bi-directional connection comprised of two unidirectional (simplex) queues, using SMP agents (A and B) to facilitate it, and two different SMP servers (which could be the same server). It is shown on the diagram above and has these steps: +The procedure of establishing a duplex connection is explained on the example of Alice and Bob creating a bi-directional connection consisting of two unidirectional (simplex) queues, using SMP agents (A and B) to facilitate it, and two different SMP servers (which could be the same server). It is shown on the diagram above and has these steps: -1. Alice requests the new connection from the SMP agent A using `NEW` command. +1. Alice requests the new connection from the SMP agent A using SMP NEW command. 2. Agent A creates an SMP connection on the server (using [SMP protocol](./simplex-messaging.md)) and responds to Alice with the invitation that contains queue information and the encryption key Bob's agent B should use. The invitation format is described in [Connection request](#connection-request). -3. Alice sends the invitation to Bob via any secure channel they have (out-of-band message). -4. Bob sends `JOIN` command with the invitation as a parameter to agent B to accept the connection. -5. Establishing Alice's SMP connection (with SMP protocol commands): - - Agent B sends an "SMP confirmation" to the SMP queue specified in the invitation - SMP confirmation is an unauthenticated message with an ephemeral key that will be used to authenticate Bob's commands to the queue, as described in SMP protocol, and Bob's info. - - Agent A receives the SMP confirmation containing Bob's key and info. +3. Alice sends the [connection request](#connection-request) to Bob via any secure channel (out-of-band message). +4. Bob sends `JOIN` command with the connection request as a parameter to agent B to accept the connection. +5. Establishing Alice's SMP queue (with SMP protocol commands): + - Agent B sends an "SMP confirmation" with SMP SEND command to the SMP queue specified in the connection request - SMP confirmation is an unauthenticated message with an ephemeral key that will be used to authenticate Bob's commands to the queue, as described in SMP protocol, and Bob's info (profile, public key for E2E encryption, etc.). This message is encrypted using key passed in the connection request (or with the derived key, in which case public key for key derivation should be sent in clear text). + - Agent A receives the SMP confirmation containing Bob's key and info as SMP MSG. - Agent A notifies Alice sending REQ notification with Bob's info. - Alice accepts connection request with ACPT command. - - Agent A secures the queue. + - Agent A secures the queue with SMP KEY command. - Agent B tries sending authenticated SMP SEND command with agent `HELLO` message until it succeeds. Once it succeeds, Bob's agent "knows" the queue is secured. 6. Agent B creates a new SMP queue on the server. 7. Establish Bob's SMP queue: - - Agent B sends `REPLY` message with the invitation to this 2nd queue to Alice's agent (via the 1st queue). - - Agent A, having received this `REPLY` message, sends unauthenticated message to SMP queue with Alice agent's ephemeral key that will be used to authenticate Alice's commands to the queue, as described in SMP protocol, and Alice's info. - - Bob's agent receives the key and Alice's information and secures the queue. + - Agent B sends `REPLY` message (SMP SEND command) with the connection request to this 2nd queue to Alice's agent (via the 1st queue) - this connection request SHOULD use "simplex" URI scheme. + - Agent A, having received `REPLY` message, sends unauthenticated message (SMP SEND) to SMP queue with Alice agent's ephemeral key that will be used to authenticate Alice's commands to the queue, as described in SMP protocol, and Alice's info. + - Bob's agent receives the key and Alice's information and secures the queue (SMP KEY). - Bob's agent sends the notification `INFO` with Alice's information to Bob. - Alice's agent keeps sending `HELLO` message until it succeeds. 8. Agents A and B notify Alice and Bob that connection is established. @@ -94,17 +94,21 @@ The procedure of establishing a duplex connection is explained on the example of At this point the duplex connection between Alice and Bob is established, they can use `SEND` command to send messages. The diagram also shows how the connection status changes for both parties, where the first part is the status of the SMP queue to receive messages, and the second part - the status of the queue to send messages. -The most communication happens between the agents and servers, from the point of view of Alice and Bob they have only 3 steps to do: +The most communication happens between the agents and servers, from the point of view of Alice and Bob there are 4 steps (not including notifications): 1. Alice requests a new connection with `NEW` command and receives the invitation. -2. Alice passes invitation out-of-band to Bob. -3. Bob accepts the connection by sending `JOIN` command with the invitation to his agent. +2. Alice passes connection request out-of-band to Bob. +3. Bob accepts the connection with `JOIN` command with the connection request to his agent. +4. Alice accepts the connection with `ACPT` command. +5. Both parties receive `CON` notification once duplex connection is established. + +Clients SHOULD support establishing duplex connection asynchronously (when parties are intermittently offline) by persisting intermediate states and resuming SMP queue subscriptions. ## Communication between SMP agents -SMP agents communicate via SMP servers managing creation, deletion and operations of SMP queues. +To establish duplex connections and to send messages on behalf of their clients, SMP agents communicate via SMP servers. -Agents can use SMP message client body (the part of the SMP message after header - see [SMP protocol](./simplex-messaging.md)) to transmit agent client messages and exchange messages between each other. +Agents use SMP message client body (the part of the SMP message after header - see [SMP protocol](./simplex-messaging.md)) to transmit agent client messages and exchange messages between each other. Each SMP message client body, once decrypted, contains 3 parts (one of them may include binary message body), as defined by `decryptedSmpMessageBody` syntax: @@ -112,8 +116,9 @@ Each SMP message client body, once decrypted, contains 3 parts (one of them may - `agentMessage` - a command/message to the other SMP agent: - to establish the connection with two SMP queues (`helloMsg`, `replyQueueMsg`) - to send and to acknowledge user messages (`clientMsg`, `acknowledgeMsg`) - - to notify another agent about queue deletion (`deleteQueueMsg`) -- `msgPadding` - an optional message padding to make all SMP messages have consistent size as an additional privacy protection measure. + - to manage SMP queue rotation (`newQueueMessage`, `deleteQueueMsg`) + - to manage encryption key rotation (TODO) +- `msgPadding` - an optional message padding to make all SMP messages have constant size, to prevent servers from observing the actual message size. ### Messages between SMP agents @@ -127,7 +132,9 @@ agentTimestamp = ; RFC3339 previousMsgHash = encoded encoded = -agentMessage = helloMsg / replyQueueMsg / deleteQueueMsg / clientMsg / acknowledgeMsg +agentMessage = helloMsg / replyQueueMsg / + clientMsg / acknowledgeMsg / + newQueueMessage / deleteQueueMsg msgPadding = *OCTET ; optional random bytes to get messages to the same size (as defined in SMP message size) @@ -135,19 +142,18 @@ helloMsg = %s"HELLO" SP signatureVerificationKey [SP %s"NO_ACK"] ; NO_ACK means that acknowledgements to client messages will NOT be sent in this connection by the agent that sent `HELLO` message. signatureVerificationKey = encoded -replyQueueMsg = %s"REPLY" SP ; `connectionRequest` is defined below +replyQueueMsg = %s"REPLY" SP connectionRequest ; `connectionRequest` is defined below ; this message can only be sent by the second connection party -deleteQueueMsg = %s"DEL" ; notification that recipient queue will be deleted -; no need to notify the other party about suspending queue separately, as suspended and deleted queues are the same to the sender -; NOT SUPPORTED with the current implementation - clientMsg = %s"MSG" SP size CRLF clientMsgBody CRLF ; CRLF is in addition to CRLF in decryptedSmpMessageBody size = 1*DIGIT clientMsgBody = *OCTET -acknowledgeMsg = %s"ACK" SP agentMsgId SP ackStatus -; NOT SUPPORTED with the current implementation +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 @@ -157,26 +163,47 @@ ackErrorType = ackUnknownMsg / ackProhibitedMsg / ackSyntaxErr ackUnknownMsg = %s"UNKNOWN" -ackProhibitedMsg = %s"PROHIBITED" ; e.g. "HELLO" or "REPLY" +ackProhibitedMsg = %s"PROHIBITED" ; unexpected message e.g. "HELLO" or "REPLY" ackSyntaxErr = %s"SYNTAX" SP syntaxErrCode syntaxErrCode = 1*DIGIT ; TODO + +newQueueMsg = %s"NEW" SP 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 +; 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 ``` #### HELLO message -This is the first message that both agents send after the respective SMP queue is secured by the receiving agent (see diagram). It contains the verification key that the sender will use to cryptographically sign the messages. +This is the first message that both agents send after the respective SMP queue is secured by the receiving agent (see diagram). It MAY contain the public key that the recipient would use to verify messages signed by the sender. -Sending agent might need to retry sending HELLO message, as it would not have any other confirmation that the queue is secured other than the success of sending this message with the signed SEND command of SMP protocol. +Sending agent might need to retry sending HELLO message, as it would not have any other confirmation that the queue is secured other than the success of sending this message with the signed SMP SEND command. #### REPLY message -This is the message that is sent by the agent that received an out-of-band invitation to pass the invitation to the reply SMP queue to the agent that originated the connection (see diagram). +This is the message that is sent by the agent that received an out-of-band connection request to pass the connection request for the reply SMP queues to the agent that originated the connection (see diagram). #### MSG message This is the agent envelope used to send client messages once the connection is established. Do not confuse it with the MSG response from SMP server to the agent and MSG response from SMP agent to the client that are sent in different contexts. +#### ACK message + +This message is sent to confirm the client message reception. It includes received message number, message hash and the reception status. + +#### NEW message + +This message is sent to add an additional SMP queue to the connection. Unlike REPLY message it can be sent at any time. + +#### DEL message + +This message is sent to notify that the queue with passed URI will be deleted - having received this message, the receiving agent should no longer send messages to this queue. In case it was the only queue in the connection to which the agent could send the messages, it MAY also delete the reply queue(s) in the connection. + ## SMP agent commands This part describes the transmissions between users and client-side SMP agents: commands that the users send to create and operate duplex connections and SMP agent responses and messages they deliver. @@ -272,9 +299,12 @@ previousMsgId = agentMsgId acknowledgeCmd = %s"ACK" SP agentMsgId ; ID assigned by receiving agent (in MSG "R") -received = %s"RCVD" SP agentMsgId ; ID assigned by sending agent (in SENT response) +received = %s"RCVD" SP agentMsgId SP msgIntegrity +; ID assigned by sending agent (in SENT response) ; currently not implemented +msgStatus = ok | error + ok = %s"OK" error = %s"ERR" SP @@ -284,13 +314,13 @@ error = %s"ERR" SP #### NEW command and INV response -`NEW` command is used to create a connection and an invitation to be sent out-of-band to another protocol user (the joining party). It should be used by the client of the agent that initiates creating a duplex connection (the initiating party). +`NEW` command is used to create a connection and a connection request to be sent out-of-band to another protocol user (the joining party). It should be used by the client of the agent that initiates creating a duplex connection (the initiating party). `INV` response is sent by the agent to the client of the initiating party. #### JOIN command -It is used to create a connection and accept the invitation received out-of-band. It should be used by the client of the agent that accepts the connection (the joining party). +It is used to create a connection and accept the connection request received out-of-band. It should be used by the client of the agent that accepts the connection (the joining party). #### REQ notification and ACPT command @@ -308,17 +338,17 @@ Once the connection is established and ready to accept client messages, both age This command can be used by the client to resume receiving messages from the connection that was created in another TCP/client session. Agent response to this command can be `OK` or `ERR` in case connection does not exist (or can only be used to send connections - e.g. when the reply queue was not created). -#### SEND command and MID, SENT and MERR responses +#### SEND command and MID, SENT, RCVD and MERR responses `SEND` command is used by the client to send messages. -`MID` notification with the message ID (the sequential message number that includes both sent and received messages in the connection) is sent to the client to confirm that the message is accepted by the agent, before it is sent to the SMP server. +`MID` response with the message ID (the sequential message number that includes both sent and received messages in the connection) is sent to the client to confirm that the message is accepted by the agent, before it is sent to the SMP server. -`SENT` response is sent by the agent to confirm that the message was delivered to the SMP server. This notification contains the same message ID as `MID` notification. `SENT` notification, depending on network availability, can be sent at any time later, potentially in the next client session. +`SENT` notification is sent by the agent to confirm that the message was delivered to at least one of SMP servers. This notification contains the same message ID as `MID` notification. `SENT` notification, depending on network availability, can be sent at any time later, potentially in the next client session. -In case of the failure to send the message for any other reason than network connection or message queue quota - e.g. authentication error (`ERR AUTH`) or syntax error (`ERR CMD error`), the agent will send to the client `MERR` notification with the message ID, and this message delivery will no longer be attempted. +`RCVD` notification is sent by the agent when it receives `ACK` message from the receiving agent. This notification contains reception status, only one successful notification will be sent, and multiple error notifications will be sent in case `ACK` had error status. -In case of client disconnecting from the agent, the pending messages will not be sent until the client re-connects to the agent and subscribes to the connection that has pending messages. +In case of the failure to send the message for any other reason than network connection or message queue quota - e.g. authentication error (`ERR AUTH`) or syntax error (`ERR CMD error`), the agent will send to the client `MERR` notification with the message ID, and this message delivery will no longer be attempted to this SMP queue. #### MSG notification @@ -347,19 +377,32 @@ It is used to delete the connection and all messages in it, as well as the recei ## Connection request -Connection request `connectionRequest` is generated by SMP agent in response to `newCmd` command (`"NEW"`), used by another party user with `joinCmd` command (`"JOIN"`), and then another invitation is sent by the agent in `replyQueueMsg` and used by the first party agent to connect to the reply queue (the second part of the process is invisible to the users). +Connection request `connectionRequest` is generated by SMP agent in response to `newCmd` command (`"NEW"`), used by another party user with `joinCmd` command (`"JOIN"`), and then another connection request is sent by the agent in `replyQueueMsg` and used by the first party agent to connect to the reply queue (the second part of the process is invisible to the users). Connection request syntax: ``` -connectionRequest = queueURI "#/connect/" encryptionScheme ":" publicKey +connectionRequest = connectionProtocol "/" action "#/?smp=" smpQueues "&e2e=" e2eEncryption +action = %s"connect" +connectionProtocol = (%s"https://" clientAppServer) | %s"simplex:" +clientAppServer = hostname [ ":" port ] +; client app server, e.g. simplex.chat +e2eEncryption = encryptionScheme ":" publicKey encryptionScheme = %s"rsa" ; end-to-end encryption and key exchange protocols, ; the current hybrid encryption scheme (RSA-OAEP/AES-256-GCM-SHA256) ; will be replaced with double ratchet protocol and DH key exchange. -publicKey = +publicKey = +smpQueues = smpQueue [ "," 1*smpQueue ] ; SMP queues for the connection +smpQueue = ``` -See SMP protocol [out-of-band messages](./simplex-messaging.md#out-of-band-messages) for syntax for queueURI. +All parameters are passed via URI hash to avoid sending them to the server (in case "https" scheme is used) - they can be used by the client-side code and processed by the client application. Parameters `smp` and `e2e` can be present in any order, any unknown additional parameters SHOULD be ignored. + +`clientAppServer` is not an SMP server - it is a server that shows the instruction on how to download the client app that will connect using this connection request. This server can also host a mobile or desktop app manifest so that this link is opened directly in the app if it is installed on the device. + +"simplex" URI scheme in `connectionProtocol` can be used instead of client app server, to connect without creating any web traffic. Client apps MUST support this URI scheme. + +See SMP protocol [out-of-band messages](./simplex-messaging.md#out-of-band-messages) for syntax of `queueURI`. [1]: https://en.wikipedia.org/wiki/End-to-end_encryption [2]: https://en.wikipedia.org/wiki/Man-in-the-middle_attack diff --git a/protocol/simplex-messaging.md b/protocol/simplex-messaging.md index d90b3353b..1ae222b1d 100644 --- a/protocol/simplex-messaging.md +++ b/protocol/simplex-messaging.md @@ -300,11 +300,11 @@ Simplex messaging clients and servers must cryptographically sign commands, resp - queue IDs response (`IDS`) - notifier queue ID response (`NID`) - delivered messages (`MSG`) - - `OK` and `ERR` responses + - `OK` and `ERR` responses (excluding error responses not related to a queue) -To sign/verify commands, messages and responses, clients and servers MUST use Ed25519 or Ed448 algorithm defined in [RFC8709][15]. +To sign/verify transmissions clients and servers MUST use Ed25519 or Ed448 algorithm defined in [RFC8709][15]. -To encrypt/decrypt message bodies delivered to the recipients, clients and servers MUST use x25519 or x448 algorithm defined in [RFC8709][15]. +To encrypt/decrypt message bodies delivered to the recipients, servers/clients MUST use x25519 or x448 algorithm defined in [RFC8709][15] to derive the shared secret (TODO encryption scheme). Clients MUST encrypt message bodies sent via SMP servers - the protocol for this end-to-end encryption should be chosen by the clients using SMP protocol. @@ -391,6 +391,8 @@ To keep the transport connection alive and to generate noise traffic the clients ping = %s"PING" ``` +This command is always send unsigned. + ### Recipient commands Sending any of the commands in this section (other than `create`, that is sent without queue ID) is only allowed with recipient's ID (`RID`). If sender's ID is used the server must respond with `"ERR AUTH"` response (see [Error responses](#error-responses)). @@ -401,17 +403,21 @@ This command is sent by the recipient to the SMP server to create a new queue. T ```abnf create = %s"NEW" SP recipientSignaturePublicKey SP recipientDhPublicKey -recipientSignaturePublicKey = signatureKey +recipientSignaturePublicKey = signaturePublicKey ; the recipient's public key to verify commands for this queue -signatureKey = signatureScheme ":" x509encoded + +signaturePublicKey = signatureScheme ":" x509encoded signatureScheme = %s"rsa" | %s"ed25519" | %s"ed448" ; "rsa" means deprecated RSA-PSS signature scheme, ; it must not be used for the new queues. + recipientDhPublicKey = dhPublicKey dhPublicKey = encryptionScheme ":" x509encoded ; the recipient's key for DH exchange to derive the secret ; that the server will use to encrypt delivered message bodies + encryptionScheme = %s"x25519" | %s"x448" +; TODO change to define the encryption scheme, e.g. "crypto_box" x509encoded = ``` @@ -425,16 +431,16 @@ serverSignaturePublicKey = signatureKey ; the server's public key to verify responses and messages for this queue serverDhPublicKey = dhPublicKey ; the server's key for DH exchange to derive the secret -; that the server will use to encrypt delivered message bodies +; that the server will use to encrypt delivered message bodies to the recipient recipientId = encoded senderId = encoded ``` -This response should be sent with empty queue ID (the third part of the transmission). - Once the queue is created, the recipient gets automatically subscribed to receive the messages from that queue, until the transport connection is closed. The `subscribe` command is needed only to start receiving the messages from the existing queue when the new transport connection is opened. -NEW `transmission` must be signed using the `recipientKey` that was passed in the transmission – this verifies that the client has the private key that will be used to sign subsequent commands for this queue. +`NEW` transmission MUST be signed using the private part of the `recipientSignaturePublicKey` – this verifies that the client has the private key that will be used to sign subsequent commands for this queue. + +`IDS` response transmission MUST be sent signed with `serverSignaturePublicKey` – this verifies that the server has the private key that will be used to sign subsequent responses and messages for this queue. This response should be sent with empty queue ID (the third part of the transmission). #### Subscribe to queue @@ -448,13 +454,16 @@ If subscription is successful the server must respond with the first available m The first message will be delivered either immediately or as soon as it is available; to receive the following message the recipient must acknowledge the reception of the message (see [Acknowledge message delivery](#acknowledge-message-delivery)). +This transmission and its response MUST be signed. + #### Secure queue command This command is sent by the recipient to the server to add sender's key to the queue: ```abnf -secure = %s"KEY" SP senderKey -senderKey = signatureScheme ":" x509encoded ; the sender's public key public key to verify SEND command for this queue +secure = %s"KEY" SP senderSignaturePublicKey +senderSignaturePublicKey = signaturePublicKey +; the sender's key to verify SEND commands for this queue ``` `senderKey` is received from the sender as part of the first message - see [Send Message](#send-message) command. From bebd1e5fb903fb30bc248f0dd8e7f05c238e8c2f Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 29 Nov 2021 23:00:09 +0000 Subject: [PATCH 09/20] simplex URI request --- protocol/simplex-uri-request.txt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 protocol/simplex-uri-request.txt diff --git a/protocol/simplex-uri-request.txt b/protocol/simplex-uri-request.txt new file mode 100644 index 000000000..968757398 --- /dev/null +++ b/protocol/simplex-uri-request.txt @@ -0,0 +1,18 @@ +Scheme name: simplex + +Status: Provisional + +Applications/protocols that use this scheme name: +This scheme is used for connection requests in SimpleX Agent Protocol, +a middle layer protocol for managing bi-directional communication via +redundant unidirectional SimpleX Messaging Protocol queues. + +Contact: Evgeny Poberezkin + +Change controller: Evgeny Poberezkin + +References: +The syntax for connection requests in the latest version of SimpleX Agent Protocol: +https://github.com/simplex-chat/simplexmq/blob/v5/protocol/agent-protocol.md#connection-request +SimpleX Messaging Protocol: +https://github.com/simplex-chat/simplexmq/blob/v5/protocol/simplex-messaging.md From 606bbf506292a061e4956c1d600fd1616b770162 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 2 Dec 2021 17:43:05 +0000 Subject: [PATCH 10/20] URI syntax for SMP queues and connection requests (#210) * URI syntax for SMP queues and connection requests * fix connection request parsing/serializing * add ConnectionRequest sqlite instances --- package.yaml | 1 + simplexmq.cabal | 7 +- src/Simplex/Messaging/Agent.hs | 35 +++--- src/Simplex/Messaging/Agent/Client.hs | 4 +- src/Simplex/Messaging/Agent/Protocol.hs | 127 ++++++++++++++++---- src/Simplex/Messaging/Agent/Store/SQLite.hs | 8 +- src/Simplex/Messaging/Parsers.hs | 23 +++- tests/AgentTests.hs | 29 +++-- tests/AgentTests/ConnectionRequestTests.hs | 69 +++++++++++ 9 files changed, 242 insertions(+), 61 deletions(-) create mode 100644 tests/AgentTests/ConnectionRequestTests.hs diff --git a/package.yaml b/package.yaml index 85eb6eb29..a32507ff3 100644 --- a/package.yaml +++ b/package.yaml @@ -39,6 +39,7 @@ dependencies: - directory == 1.3.* - file-embed == 0.0.14.* - filepath == 1.4.* + - http-types == 0.12.* - generic-random >= 1.3 && < 1.5 - iso8601-time == 0.1.* - memory == 0.15.* diff --git a/simplexmq.cabal b/simplexmq.cabal index 82e187ce0..4bd0b59f0 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -4,7 +4,7 @@ cabal-version: 1.12 -- -- see: https://github.com/sol/hpack -- --- hash: 706d2f9155c3f3be0f08ea0d6c8954c0e2b9a6e22615f7b19499a3a349af7cc9 +-- hash: eac6184d7efe4fc07606d5f6bd75f1821af768a087526ff08749385844580cec name: simplexmq version: 0.4.1 @@ -81,6 +81,7 @@ library , file-embed ==0.0.14.* , filepath ==1.4.* , generic-random >=1.3 && <1.5 + , http-types ==0.12.* , iso8601-time ==0.1.* , memory ==0.15.* , mtl ==2.2.* @@ -126,6 +127,7 @@ executable smp-agent , file-embed ==0.0.14.* , filepath ==1.4.* , generic-random >=1.3 && <1.5 + , http-types ==0.12.* , iso8601-time ==0.1.* , memory ==0.15.* , mtl ==2.2.* @@ -173,6 +175,7 @@ executable smp-server , file-embed ==0.0.14.* , filepath ==1.4.* , generic-random >=1.3 && <1.5 + , http-types ==0.12.* , ini ==0.4.* , iso8601-time ==0.1.* , memory ==0.15.* @@ -200,6 +203,7 @@ test-suite smp-server-test main-is: Test.hs other-modules: AgentTests + AgentTests.ConnectionRequestTests AgentTests.FunctionalAPITests AgentTests.SQLiteTests ProtocolErrorTests @@ -232,6 +236,7 @@ test-suite smp-server-test , generic-random >=1.3 && <1.5 , hspec ==2.7.* , hspec-core ==2.7.* + , http-types ==0.12.* , iso8601-time ==0.1.* , memory ==0.15.* , mtl ==2.2.* diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 18dd0d845..3c14cbbff 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -7,6 +7,7 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NumericUnderscores #-} +{-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE RecordWildCards #-} @@ -128,11 +129,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 -> m (ConnId, SMPQueueInfo) +createConnection :: AgentErrorMonad m => AgentClient -> m (ConnId, ConnectionRequest) createConnection c = withAgentEnv c $ newConn c "" -- | Join SMP agent connection (JOIN command) -joinConnection :: AgentErrorMonad m => AgentClient -> SMPQueueInfo -> ConnInfo -> m ConnId +joinConnection :: AgentErrorMonad m => AgentClient -> ConnectionRequest -> ConnInfo -> m ConnId joinConnection c = withAgentEnv c .: joinConn c "" -- | Approve confirmation (LET command) @@ -236,7 +237,7 @@ withStore action = do processCommand :: forall m. AgentMonad m => AgentClient -> (ConnId, ACommand 'Client) -> m (ConnId, ACommand 'Agent) processCommand c (connId, cmd) = case cmd of NEW -> second INV <$> newConn c connId - JOIN smpQueueInfo connInfo -> (,OK) <$> joinConn c connId smpQueueInfo connInfo + JOIN smpQueueUri connInfo -> (,OK) <$> joinConn c connId smpQueueUri connInfo ACPT confId ownConnInfo -> acceptConnection' c connId confId ownConnInfo $> (connId, OK) SUB -> subscribeConnection' c connId $> (connId, OK) SEND msgBody -> (connId,) . MID <$> sendMessage' c connId msgBody @@ -244,19 +245,19 @@ processCommand c (connId, cmd) = case cmd of OFF -> suspendConnection' c connId $> (connId, OK) DEL -> deleteConnection' c connId $> (connId, OK) -newConn :: AgentMonad m => AgentClient -> ConnId -> m (ConnId, SMPQueueInfo) +newConn :: AgentMonad m => AgentClient -> ConnId -> m (ConnId, ConnectionRequest) newConn c connId = do srv <- getSMPServer - (rq, qInfo) <- newRcvQueue c srv + (rq, qUri, encryptKey) <- newRcvQueue c srv g <- asks idsDrg let cData = ConnData {connId} connId' <- withStore $ \st -> createRcvConn st g cData rq addSubscription c rq connId' - pure (connId', qInfo) + pure (connId', ConnectionRequest simplexChat CRAConnect [qUri] encryptKey) -joinConn :: AgentMonad m => AgentClient -> ConnId -> SMPQueueInfo -> ConnInfo -> m ConnId -joinConn c connId qInfo cInfo = do - (sq, senderKey, verifyKey) <- newSndQueue qInfo +joinConn :: AgentMonad m => AgentClient -> ConnId -> ConnectionRequest -> ConnInfo -> m ConnId +joinConn c connId (ConnectionRequest _ CRAConnect (qUri :| _) encryptKey) cInfo = do + (sq, senderKey, verifyKey) <- newSndQueue qUri encryptKey g <- asks idsDrg cfg <- asks config let cData = ConnData {connId} @@ -272,10 +273,10 @@ activateQueueJoining c connId sq verifyKey retryInterval = createReplyQueue :: m () createReplyQueue = do srv <- getSMPServer - (rq, qInfo') <- newRcvQueue c srv + (rq, qUri', encryptKey) <- newRcvQueue c srv addSubscription c rq connId withStore $ \st -> upgradeSndConnToDuplex st connId rq - sendControlMessage c sq $ REPLY qInfo' + sendControlMessage c sq . REPLY $ ConnectionRequest CRSSimplex CRAConnect [qUri'] encryptKey -- | Approve confirmation (LET command) in Reader monad acceptConnection' :: AgentMonad m => AgentClient -> ConnId -> ConfirmationId -> ConnInfo -> m () @@ -513,7 +514,7 @@ processSMPTransmission c@AgentClient {subQ} (srv, rId, cmd) = do Right SMPMessage {agentMessage, senderMsgId, senderTimestamp, previousMsgHash} -> case agentMessage of HELLO verifyKey _ -> helloMsg verifyKey msgBody >> sendAck c rq - REPLY qInfo -> replyMsg qInfo >> sendAck c rq + REPLY cReq -> replyMsg cReq >> sendAck c rq A_MSG body -> agentClientMsg previousMsgHash (senderMsgId, senderTimestamp) (srvMsgId, srvTs) body msgHash SMP.END -> do removeSubscription c connId @@ -557,13 +558,13 @@ processSMPTransmission c@AgentClient {subQ} (srv, rId, cmd) = do SCDuplex -> notifyConnected c connId _ -> pure () - replyMsg :: SMPQueueInfo -> m () - replyMsg qInfo = do + replyMsg :: ConnectionRequest -> m () + replyMsg (ConnectionRequest _ CRAConnect (qUri :| _) encryptKey) = do logServer "<--" c srv rId "MSG " case cType of SCRcv -> do AcceptedConfirmation {ownConnInfo} <- withStore (`getAcceptedConfirmation` connId) - (sq, senderKey, verifyKey) <- newSndQueue qInfo + (sq, senderKey, verifyKey) <- newSndQueue qUri encryptKey withStore $ \st -> upgradeRcvConnToDuplex st connId sq confirmQueue c sq senderKey ownConnInfo withStore (`removeConfirmations` connId) @@ -623,8 +624,8 @@ notifyConnected :: AgentMonad m => AgentClient -> ConnId -> m () notifyConnected c connId = atomically $ writeTBQueue (subQ c) ("", connId, CON) newSndQueue :: - (MonadUnliftIO m, MonadReader Env m) => SMPQueueInfo -> m (SndQueue, SenderPublicKey, VerificationKey) -newSndQueue (SMPQueueInfo smpServer senderId encryptKey) = do + (MonadUnliftIO m, MonadReader Env m) => SMPQueueUri -> EncryptionKey -> m (SndQueue, SenderPublicKey, VerificationKey) +newSndQueue (SMPQueueUri smpServer senderId _) encryptKey = do size <- asks $ rsaKeySize . config (senderKey, sndPrivateKey) <- liftIO $ C.generateKeyPair size (verifyKey, signKey) <- liftIO $ C.generateKeyPair size diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index cc9cfb340..bc057b31f 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -226,7 +226,7 @@ smpClientError = \case SMPTransportError e -> BROKER $ TRANSPORT e e -> INTERNAL $ show e -newRcvQueue :: AgentMonad m => AgentClient -> SMPServer -> m (RcvQueue, SMPQueueInfo) +newRcvQueue :: AgentMonad m => AgentClient -> SMPServer -> m (RcvQueue, SMPQueueUri, EncryptionKey) newRcvQueue c srv = do size <- asks $ rsaKeySize . config (recipientKey, rcvPrivateKey) <- liftIO $ C.generateKeyPair size @@ -244,7 +244,7 @@ newRcvQueue c srv = do verifyKey = Nothing, status = New } - return (rq, SMPQueueInfo srv sId encryptKey) + pure (rq, SMPQueueUri srv sId reservedServerKey, encryptKey) subscribeQueue :: AgentMonad m => AgentClient -> RcvQueue -> ConnId -> m () subscribeQueue c rq@RcvQueue {server, rcvPrivateKey, rcvId} connId = do diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index 44e42cdd4..8ae384ca5 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -37,7 +37,11 @@ module Simplex.Messaging.Agent.Protocol SMPMessage (..), AMessage (..), SMPServer (..), - SMPQueueInfo (..), + SMPQueueUri (..), + ConnectionRequest (..), + ConnReqScheme (..), + simplexChat, + ConnReqAction (..), AgentErrorType (..), CommandErrorType (..), ConnectionErrorType (..), @@ -67,14 +71,18 @@ module Simplex.Messaging.Agent.Protocol serializeSMPMessage, serializeMsgIntegrity, serializeServer, - serializeSmpQueueInfo, + serializeSMPQueueUri, + reservedServerKey, -- TODO remove + serializeConnReq, serializeAgentError, commandP, parseSMPMessage, smpServerP, - smpQueueInfoP, + smpQueueUriP, + connReqP, msgIntegrityP, agentErrorTypeP, + agentMessageP, -- * TCP transport functions tPut, @@ -86,14 +94,18 @@ where import Control.Applicative (optional, (<|>)) import Control.Monad.IO.Class +import qualified Crypto.PubKey.RSA as R 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.Functor (($>)) import Data.Int (Int64) import Data.Kind (Type) +import Data.List (find) +import qualified Data.List.NonEmpty as L import Data.String (IsString (..)) import Data.Time.Clock (UTCTime) import Data.Time.ISO8601 @@ -101,6 +113,7 @@ import Data.Type.Equality import Data.Typeable () import GHC.Generics (Generic) import Generic.Random (genericArbitraryU) +import Network.HTTP.Types (parseSimpleQuery, renderSimpleQuery) import Network.Socket (HostName, ServiceName) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Parsers @@ -155,8 +168,8 @@ type ConnInfo = ByteString -- | Parameterized type for SMP agent protocol commands and responses from all participants. data ACommand (p :: AParty) where NEW :: ACommand Client -- response INV - INV :: SMPQueueInfo -> ACommand Agent - JOIN :: SMPQueueInfo -> ConnInfo -> ACommand Client -- response OK + INV :: ConnectionRequest -> ACommand Agent + JOIN :: ConnectionRequest -> ConnInfo -> ACommand Client -- response OK REQ :: ConfirmationId -> ConnInfo -> ACommand Agent -- ConnInfo is from sender ACPT :: ConfirmationId -> ConnInfo -> ACommand Client -- ConnInfo is from client INFO :: ConnInfo -> ACommand Agent @@ -225,7 +238,7 @@ data AMessage where -- | the first message in the queue to validate it is secured HELLO :: VerificationKey -> AckMode -> AMessage -- | reply queue information - REPLY :: SMPQueueInfo -> AMessage + REPLY :: ConnectionRequest -> AMessage -- | agent envelope for the client message A_MSG :: MsgBody -> AMessage deriving (Show) @@ -270,15 +283,10 @@ agentMessageP = <|> "MSG " *> a_msg where hello = HELLO <$> C.pubKeyP <*> ackMode - reply = REPLY <$> smpQueueInfoP + reply = REPLY <$> connReqP a_msg = A_MSG <$> binaryBodyP <* A.endOfLine ackMode = AckMode <$> (" NO_ACK" $> Off <|> pure On) --- | SMP queue information parser. -smpQueueInfoP :: Parser SMPQueueInfo -smpQueueInfoP = - "smp::" *> (SMPQueueInfo <$> smpServerP <* "::" <*> base64P <* "::" <*> C.pubKeyP) - -- | SMP server location parser. smpServerP :: Parser SMPServer smpServerP = SMPServer <$> server <*> optional port <*> optional kHash @@ -290,19 +298,74 @@ smpServerP = SMPServer <$> server <*> optional port <*> optional kHash serializeAgentMessage :: AMessage -> ByteString serializeAgentMessage = \case HELLO verifyKey ackMode -> "HELLO " <> C.serializePubKey verifyKey <> if ackMode == AckMode Off then " NO_ACK" else "" - REPLY qInfo -> "REPLY " <> serializeSmpQueueInfo qInfo + REPLY cReq -> "REPLY " <> serializeConnReq cReq A_MSG body -> "MSG " <> serializeBinary body <> "\n" -- | Serialize SMP queue information that is sent out-of-band. -serializeSmpQueueInfo :: SMPQueueInfo -> ByteString -serializeSmpQueueInfo (SMPQueueInfo srv qId ek) = - B.intercalate "::" ["smp", serializeServer srv, encode qId, C.serializePubKey ek] +serializeSMPQueueUri :: SMPQueueUri -> ByteString +serializeSMPQueueUri (SMPQueueUri srv qId _) = + serializeServerUri srv <> "/" <> U.encode qId <> "#" + +-- | SMP queue information parser. +smpQueueUriP :: Parser SMPQueueUri +smpQueueUriP = + SMPQueueUri <$> smpServerUriP <* "/" <*> base64UriP <* "#" <*> pure reservedServerKey + +reservedServerKey :: C.PublicKey +reservedServerKey = C.PublicKey $ R.PublicKey 1 0 0 + +serializeConnReq :: ConnectionRequest -> ByteString +serializeConnReq (ConnectionRequest scheme action smpQueues encryptionKey) = + sch <> "/" <> act <> "#/" <> queryStr + where + sch = case scheme of + CRSSimplex -> "simplex:" + CRSAppServer host port -> B.pack $ "https://" <> host <> maybe "" (':' :) port + act = case action of + CRAConnect -> "connect" + queryStr = renderSimpleQuery True [("smp", queues), ("e2e", key)] + queues = B.intercalate "," . map serializeSMPQueueUri $ L.toList smpQueues + key = C.serializePubKey encryptionKey + +connReqP :: Parser ConnectionRequest +connReqP = do + crScheme <- "simplex:" $> CRSSimplex <|> "https://" *> appServer + crAction <- "/" *> ("connect" $> CRAConnect) <* "#/?" + query <- parseSimpleQuery <$> A.takeTill (\c -> c == ' ' || c == '\n') + crSmpQueues <- paramP "smp" smpQueues query + crEncryptKey <- paramP "e2e" C.pubKeyP query + pure ConnectionRequest {crScheme, crAction, crSmpQueues, crEncryptKey} + where + appServer = CRSAppServer <$> host <*> optional port + host = B.unpack <$> A.takeTill (\c -> c == ':' || c == '/') + port = B.unpack <$> (A.char ':' *> A.takeTill (== '/')) + paramP param parser query = + let p = maybe (fail "") (pure . snd) $ find ((== param) . fst) query + in parseAll parser <$?> p + smpQueues = + maybe (fail "no SMP queues") pure . L.nonEmpty + =<< (smpQueue `A.sepBy1'` A.char ',') + smpQueue = parseAll smpQueueUriP <$?> A.takeTill (== ',') -- | Serialize SMP server location. serializeServer :: SMPServer -> ByteString serializeServer SMPServer {host, port, keyHash} = B.pack $ host <> maybe "" (':' :) port <> maybe "" (('#' :) . B.unpack . encode . C.unKeyHash) keyHash +serializeServerUri :: SMPServer -> ByteString +serializeServerUri SMPServer {host, port, keyHash} = "smp://" <> kh <> B.pack host <> p + where + kh = maybe "" ((<> "@") . U.encode . C.unKeyHash) keyHash + p = B.pack $ maybe "" (':' :) port + +smpServerUriP :: Parser SMPServer +smpServerUriP = do + _ <- "smp://" + keyHash <- optional $ C.KeyHash <$> (U.decode <$?> A.takeTill (== '@') <* A.char '@') + host <- B.unpack <$> A.takeWhile1 (A.notInClass ":#,;/ ") + port <- optional $ B.unpack <$> (A.char ':' *> A.takeWhile1 A.isDigit) + pure SMPServer {host, port, keyHash} + -- | SMP server location and transport key digest (hash). data SMPServer = SMPServer { host :: HostName, @@ -332,9 +395,29 @@ newtype AckMode = AckMode OnOff deriving (Eq, Show) -- | SMP queue information sent out-of-band. -- -- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#out-of-band-messages -data SMPQueueInfo = SMPQueueInfo SMPServer SMP.SenderId EncryptionKey +data SMPQueueUri = SMPQueueUri + { smpServer :: SMPServer, + senderId :: SMP.SenderId, + serverVerifyKey :: VerificationKey + } deriving (Eq, Show) +data ConnectionRequest = ConnectionRequest + { crScheme :: ConnReqScheme, + crAction :: ConnReqAction, + crSmpQueues :: L.NonEmpty SMPQueueUri, + crEncryptKey :: EncryptionKey + } + deriving (Eq, Show) + +data ConnReqScheme = CRSSimplex | CRSAppServer HostName (Maybe ServiceName) + deriving (Eq, Show) + +simplexChat :: ConnReqScheme +simplexChat = CRSAppServer "simplex.chat" Nothing + +data ConnReqAction = CRAConnect deriving (Eq, Show) + -- | Public key used to E2E encrypt SMP messages. type EncryptionKey = C.PublicKey @@ -476,8 +559,8 @@ commandP = <|> "CON" $> ACmd SAgent CON <|> "OK" $> ACmd SAgent OK where - invResp = ACmd SAgent . INV <$> smpQueueInfoP - joinCmd = ACmd SClient <$> (JOIN <$> smpQueueInfoP <* A.space <*> A.takeByteString) + invResp = ACmd SAgent . INV <$> connReqP + joinCmd = ACmd SClient <$> (JOIN <$> connReqP <* A.space <*> A.takeByteString) reqCmd = ACmd SAgent <$> (REQ <$> A.takeTill (== ' ') <* A.space <*> A.takeByteString) acptCmd = ACmd SClient <$> (ACPT <$> A.takeTill (== ' ') <* A.space <*> A.takeByteString) infoCmd = ACmd SAgent . INFO <$> A.takeByteString @@ -513,8 +596,8 @@ parseCommand = parse commandP $ CMD SYNTAX serializeCommand :: ACommand p -> ByteString serializeCommand = \case NEW -> "NEW" - INV qInfo -> "INV " <> serializeSmpQueueInfo qInfo - JOIN qInfo cInfo -> "JOIN " <> serializeSmpQueueInfo qInfo <> " " <> serializeBinary cInfo + INV cReq -> "INV " <> serializeConnReq cReq + JOIN cReq cInfo -> "JOIN " <> serializeConnReq cReq <> " " <> serializeBinary cInfo REQ confId cInfo -> "REQ " <> confId <> " " <> serializeBinary cInfo ACPT confId cInfo -> "ACPT " <> confId <> " " <> serializeBinary cInfo INFO cInfo -> "INFO " <> serializeBinary cInfo @@ -630,7 +713,7 @@ tGet party h = liftIO (tGetRaw h) >>= tParseLoadBody cmdWithMsgBody = \case SEND body -> SEND <$$> getBody body MSG msgMeta body -> MSG msgMeta <$$> getBody body - JOIN qInfo cInfo -> JOIN qInfo <$$> getBody cInfo + JOIN qUri cInfo -> JOIN qUri <$$> getBody cInfo REQ confId cInfo -> REQ confId <$$> getBody cInfo ACPT confId cInfo -> ACPT confId <$$> getBody cInfo INFO cInfo -> INFO <$$> getBody cInfo diff --git a/src/Simplex/Messaging/Agent/Store/SQLite.hs b/src/Simplex/Messaging/Agent/Store/SQLite.hs index 5d63af27c..e76729069 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite.hs @@ -501,9 +501,13 @@ instance ToField MsgIntegrity where toField = toField . serializeMsgIntegrity instance FromField MsgIntegrity where fromField = blobFieldParser msgIntegrityP -instance ToField SMPQueueInfo where toField = toField . serializeSmpQueueInfo +instance ToField SMPQueueUri where toField = toField . serializeSMPQueueUri -instance FromField SMPQueueInfo where fromField = blobFieldParser smpQueueInfoP +instance FromField SMPQueueUri where fromField = blobFieldParser smpQueueUriP + +instance ToField ConnectionRequest where toField = toField . serializeConnReq + +instance FromField ConnectionRequest where fromField = blobFieldParser connReqP fromTextField_ :: (E.Typeable a) => (Text -> Maybe a) -> Field -> Ok a fromTextField_ fromText = \case diff --git a/src/Simplex/Messaging/Parsers.hs b/src/Simplex/Messaging/Parsers.hs index 5e7741c29..8e82852b8 100644 --- a/src/Simplex/Messaging/Parsers.hs +++ b/src/Simplex/Messaging/Parsers.hs @@ -7,6 +7,7 @@ import Data.Attoparsec.ByteString.Char8 (Parser) import qualified Data.Attoparsec.ByteString.Char8 as A import Data.Bifunctor (first) 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.Char (isAlphaNum) @@ -24,10 +25,24 @@ base64P :: Parser ByteString base64P = decode <$?> base64StringP base64StringP :: Parser ByteString -base64StringP = do - str <- A.takeWhile1 (\c -> isAlphaNum c || c == '+' || c == '/') - pad <- A.takeWhile (== '=') - pure $ str <> pad +base64StringP = paddedBase64 rawBase64P + +base64UriP :: Parser ByteString +base64UriP = U.decode <$?> base64UriStringP + +base64UriStringP :: Parser ByteString +base64UriStringP = paddedBase64 rawBase64UriP + +paddedBase64 :: Parser ByteString -> Parser ByteString +paddedBase64 raw = (<>) <$> raw <*> pad + where + pad = A.takeWhile (== '=') + +rawBase64P :: Parser ByteString +rawBase64P = A.takeWhile1 (\c -> isAlphaNum c || c == '+' || c == '/') + +rawBase64UriP :: Parser ByteString +rawBase64UriP = A.takeWhile1 (\c -> isAlphaNum c || c == '-' || c == '_') tsISO8601P :: Parser UTCTime tsISO8601P = maybe (fail "timestamp") pure . parseISO8601 . B.unpack =<< A.takeTill wordEnd diff --git a/tests/AgentTests.hs b/tests/AgentTests.hs index 73dd732c2..0777b7898 100644 --- a/tests/AgentTests.hs +++ b/tests/AgentTests.hs @@ -10,11 +10,13 @@ module AgentTests (agentTests) where +import AgentTests.ConnectionRequestTests import AgentTests.FunctionalAPITests (functionalAPITests) import AgentTests.SQLiteTests (storeTests) import Control.Concurrent import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B +import Network.HTTP.Types (urlEncode) import SMPAgentClient import SMPClient (testPort, testPort2, testStoreLogFile, withSmpServer, withSmpServerStoreLogOn) import Simplex.Messaging.Agent.Protocol @@ -26,6 +28,7 @@ import Test.Hspec agentTests :: ATransport -> Spec agentTests (ATransport t) = do + describe "Connection request" connectionRequestTests describe "Functional API" $ functionalAPITests (ATransport t) describe "SQLite store" storeTests describe "SMP agent protocol syntax" $ syntaxTests t @@ -98,9 +101,9 @@ pattern Msg msgBody <- MSG MsgMeta {integrity = MsgOk} msgBody testDuplexConnection :: Transport c => TProxy c -> c -> c -> IO () testDuplexConnection _ alice bob = do - ("1", "bob", Right (INV qInfo)) <- alice #: ("1", "bob", "NEW") - let qInfo' = serializeSmpQueueInfo qInfo - bob #: ("11", "alice", "JOIN " <> qInfo' <> " 14\nbob's connInfo") #> ("11", "alice", OK) + ("1", "bob", Right (INV cReq)) <- alice #: ("1", "bob", "NEW") + let cReq' = serializeConnReq cReq + bob #: ("11", "alice", "JOIN " <> cReq' <> " 14\nbob's connInfo") #> ("11", "alice", OK) ("", "bob", Right (REQ confId "bob's connInfo")) <- (alice <#:) alice #: ("2", "bob", "ACPT " <> confId <> " 16\nalice's connInfo") #> ("2", "bob", OK) bob <# ("", "alice", INFO "alice's connInfo") @@ -130,9 +133,9 @@ testDuplexConnection _ alice bob = do testDuplexConnRandomIds :: Transport c => TProxy c -> c -> c -> IO () testDuplexConnRandomIds _ alice bob = do - ("1", bobConn, Right (INV qInfo)) <- alice #: ("1", "", "NEW") - let qInfo' = serializeSmpQueueInfo qInfo - ("11", aliceConn, Right OK) <- bob #: ("11", "", "JOIN " <> qInfo' <> " 14\nbob's connInfo") + ("1", bobConn, Right (INV cReq)) <- alice #: ("1", "", "NEW") + let cReq' = serializeConnReq cReq + ("11", aliceConn, Right OK) <- bob #: ("11", "", "JOIN " <> cReq' <> " 14\nbob's connInfo") ("", bobConn', Right (REQ confId "bob's connInfo")) <- (alice <#:) bobConn' `shouldBe` bobConn alice #: ("2", bobConn, "ACPT " <> confId <> " 16\nalice's connInfo") =#> \case ("2", c, OK) -> c == bobConn; _ -> False @@ -250,9 +253,9 @@ testMsgDeliveryAgentRestart t bob = do connect :: forall c. Transport c => (c, ByteString) -> (c, ByteString) -> IO () connect (h1, name1) (h2, name2) = do - ("c1", _, Right (INV qInfo)) <- h1 #: ("c1", name2, "NEW") - let qInfo' = serializeSmpQueueInfo qInfo - h2 #: ("c2", name1, "JOIN " <> qInfo' <> " 5\ninfo2") #> ("c2", name1, OK) + ("c1", _, Right (INV cReq)) <- h1 #: ("c1", name2, "NEW") + let cReq' = serializeConnReq cReq + h2 #: ("c2", name1, "JOIN " <> cReq' <> " 5\ninfo2") #> ("c2", name1, OK) ("", _, Right (REQ connId "info2")) <- (h1 <#:) h1 #: ("c3", name2, "ACPT " <> connId <> " 5\ninfo1") #> ("c3", name2, OK) h2 <# ("", name1, INFO "info1") @@ -261,9 +264,9 @@ connect (h1, name1) (h2, name2) = do -- connect' :: forall c. Transport c => c -> c -> IO (ByteString, ByteString) -- connect' h1 h2 = do --- ("c1", conn2, Right (INV qInfo)) <- h1 #: ("c1", "", "NEW") --- let qInfo' = serializeSmpQueueInfo qInfo --- ("c2", conn1, Right OK) <- h2 #: ("c2", "", "JOIN " <> qInfo' <> " 5\ninfo2") +-- ("c1", conn2, Right (INV cReq)) <- h1 #: ("c1", "", "NEW") +-- let cReq' = serializeConnReq cReq +-- ("c2", conn1, Right OK) <- h2 #: ("c2", "", "JOIN " <> cReq' <> " 5\ninfo2") -- ("", _, Right (REQ connId "info2")) <- (h1 <#:) -- h1 #: ("c3", conn2, "ACPT " <> connId <> " 5\ninfo1") =#> \case ("c3", c, OK) -> c == conn2; _ -> False -- h2 <# ("", conn1, INFO "info1") @@ -290,7 +293,7 @@ syntaxTests t = 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", "JOIN smp::localhost:5000::1234::" <> samplePublicKey <> " 14\nbob's connInfo") >#> ("311", "a", "ERR SMP AUTH") + ("311", "a", "JOIN https://simpex.chat/connect#/?smp=smp%3A%2F%2Flocalhost%3A5000%2F1234-w%3D%3D%23&e2e=" <> urlEncode True samplePublicKey <> " 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") diff --git a/tests/AgentTests/ConnectionRequestTests.hs b/tests/AgentTests/ConnectionRequestTests.hs new file mode 100644 index 000000000..2355b5931 --- /dev/null +++ b/tests/AgentTests/ConnectionRequestTests.hs @@ -0,0 +1,69 @@ +{-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE OverloadedStrings #-} + +module AgentTests.ConnectionRequestTests where + +import Simplex.Messaging.Agent.Protocol +import qualified Simplex.Messaging.Crypto as C +import Simplex.Messaging.Parsers (parseAll) +import Test.Hspec + +uri :: String +uri = "smp.simplex.im" + +srv :: SMPServer +srv = + SMPServer + { host = "smp.simplex.im", + port = Just "5223", + keyHash = Just (C.KeyHash "\215m\248\251") + } + +queue :: SMPQueueUri +queue = + SMPQueueUri + { smpServer = srv, + senderId = "\215m\248\251", + serverVerifyKey = reservedServerKey + } + +appServer :: ConnReqScheme +appServer = CRSAppServer "simplex.chat" Nothing + +connReq :: ConnectionRequest +connReq = + ConnectionRequest + { crScheme = appServer, + crAction = CRAConnect, + crSmpQueues = [queue], + crEncryptKey = reservedServerKey + } + +connectionRequestTests :: Spec +connectionRequestTests = do + describe "connection request parsing / serializing" $ do + it "should serialize SMP queue URIs" $ do + serializeSMPQueueUri queue {smpServer = srv {port = Nothing, keyHash = Nothing}} + `shouldBe` "smp://smp.simplex.im/1234-w==#" + serializeSMPQueueUri queue {smpServer = srv {keyHash = Nothing}} + `shouldBe` "smp://smp.simplex.im:5223/1234-w==#" + serializeSMPQueueUri queue {smpServer = srv {port = Nothing}} + `shouldBe` "smp://1234-w==@smp.simplex.im/1234-w==#" + serializeSMPQueueUri queue + `shouldBe` "smp://1234-w==@smp.simplex.im:5223/1234-w==#" + it "should parse SMP queue URIs" $ do + parseAll smpQueueUriP "smp://smp.simplex.im/1234-w==#" + `shouldBe` Right queue {smpServer = srv {port = Nothing, keyHash = Nothing}} + parseAll smpQueueUriP "smp://smp.simplex.im:5223/1234-w==#" + `shouldBe` Right queue {smpServer = srv {keyHash = Nothing}} + parseAll smpQueueUriP "smp://1234-w==@smp.simplex.im/1234-w==#" + `shouldBe` Right queue {smpServer = srv {port = Nothing}} + parseAll smpQueueUriP "smp://1234-w==@smp.simplex.im:5223/1234-w==#" + `shouldBe` Right queue + it "should serialize connection requests" $ do + serializeConnReq connReq + `shouldBe` "https://simplex.chat/connect#/?smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F1234-w%3D%3D%23&e2e=rsa%3AMBowDQYJKoZIhvcNAQEBBQADCQAwBgIBAAIBAA%3D%3D" + it "should parse connection requests" $ do + -- print $ parseSMPMessage "\n0 2021-11-29T19:23:27.005Z \nREPLY simplex:/connect#/?smp=smp%3A%2F%2FKXNE1m2E1m0lm92WGKet9CL6-lO742Vy5G6nsrkvgs8%3D%40localhost%3A5000%2FoWWCE_5ug0t05K6X%23&e2e=rsa%3AMIIBoDANBgkqhkiG9w0BAQEFAAOCAY0AMIIBiAKCAQEA3O4frbgUMRO%2B8FIX2%2ByqB%2F1B5pXmt%2F%2FY0dFd2HCVxL31TJHc90HJp92Qb7Ni%2B1dI2Ka1Hb1Fvup897mmEcFhZStG0OB6jffvPyxXCas8Tov3l757qCUZKqgTxSJkL7JvLkIN9jMs50islvrSHCAj8VReh5oR%2B8OFp8ITd5MuMHYuR1bt0XLl1TwSIyfRSQqtHlt%2FEBbEbWcgJMsDXMi3o983nezvF9En9F7OCnasdzKAsgcN2%2FdWp3CPeuMNe9epzrirxGfCKU%2FlVyZ77e7NZMkSmeOIDPGuE4Fk8bweAYArV%2FrECBJGBQkGx3YtEh0kIbCakQ1ZnKY%2F%2FMq0ZHGPhQKBgGRKfJ7xXftoLdVJ7EOW%2FR5Y%2Bj%2F%2Bb9yZMbTCdZfkuroV9FH8GF5tS3PWuSAOFu42h7TiqFjXlvM6aYp%2FBXxCosZjBlB6mWCLyuY48ZszhtCpLSlbR2x%2FpGMUEgyOsefeMusrHEqFJAI%2Fhh8LljBGL%2BV08qcGFxVTwCVePIjDOo1H\n" + parseAll connReqP "https://simplex.chat/connect#/?smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F1234-w%3D%3D%23&e2e=rsa%3AMBowDQYJKoZIhvcNAQEBBQADCQAwBgIBAAIBAA%3D%3D" + `shouldBe` Right connReq From 316dc7b320c3d42fb236cdeab1e0c1e41e52bf00 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 2 Dec 2021 18:42:13 +0000 Subject: [PATCH 11/20] merge protocol changes from v5 (#211) --- protocol/agent-protocol.md | 180 +++++++++++++-------- protocol/simplex-messaging.md | 286 +++++++++++++++++++++++----------- 2 files changed, 313 insertions(+), 153 deletions(-) diff --git a/protocol/agent-protocol.md b/protocol/agent-protocol.md index 9d80beef0..00dbc873b 100644 --- a/protocol/agent-protocol.md +++ b/protocol/agent-protocol.md @@ -24,71 +24,68 @@ - [END notification](#end-notification) - [OFF command](#off-command) - [DEL command](#del-command) -- [Connection invitation](#connection-invitation) +- [Connection request](#connection-request) ## Abstract The purpose of SMP agent protocol is to define the syntax and the semantics of communications between the client and the agent that connects to [SMP](./simplex-messaging.md) servers. It provides: -- convenient protocol to create and manage bi-directional (duplex) connections between the users of SMP agents consisting of two (or more) separate unidirectional (simplex) SMP queues, abstracting away multiple steps required to establish bi-directional connections and any information about the servers location from the users of the protocol. -- management of E2E encryption between SMP agents, generating ephemeral RSA keys for each connection. -- SMP command authentication on SMP servers, generating ephemeral RSA keys for each SMP queue. -- TCP transport handshake and encryption with SMP servers. +- protocol to create and manage bi-directional (duplex) connections between the users of SMP agents consisting of two (or more) separate unidirectional (simplex) SMP queues, abstracting away multiple steps required to establish bi-directional connections and any information about the servers location from the users of the agent protocol. +- management of E2E encryption between SMP agents, generating ephemeral asymmetric keys for each connection. +- SMP command authentication on SMP servers, generating ephemeral keys for each SMP queue. +- TCP/TLS transport handshake with SMP servers. - validation of message integrity. -SMP agent protocols provides no encryption or any security on the client side - it is assumed that the agent is executed in the trusted and secure environment. - -The future versions of this protocol could provide: -- managing redundant SMP queues with more than 1 queue in each direction. -- managing simple symmetric groups as a foundation for chat groups and device synchronization. -- agent cluster - synchronizing states of multiple agents. -- secure "synchronous" streams with symmetric message encryption and connection-level authentication (requires extending [SMP protocol](./simplex-messaging.md)) - it can be used, e.g., for file transfers. +SMP agent protocol provides no encryption or security on the client side - it is assumed that the agent is executed in the trusted and secure environment, in one of three ways: +- via TCP network using secure connection. +- via local port (when the agent runs on the same device as a separate process). +- via agent library, when the agent logic is included directly into the client application - [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat) uses this approach. ## SMP agent -SMP agent is a client-side process or library that communicates via SMP servers using [simplex messaging protocol (SMP)](./simplex-messaging.md) with other SMP agents according to the commands received from its users. This protocol is a middle layer in SMP protocols stack (above SMP protocol but below any application level protocol) - it is intended to be used by client-side applications that need secure asynchronous bi-directional communication channels ("connections"). +SMP agents communicate with each other via SMP servers using [simplex messaging protocol (SMP)](./simplex-messaging.md) according to the commands received from its users. This protocol is a middle layer in SimpleX protocols (above SMP protocol but below any application level protocol) - it is intended to be used by client-side applications that need secure asynchronous bi-directional communication channels ("connections"). -The agent must have a persistent storage to manage the states of known connections and of the client-side information of two SMP queues that each connection consists of, and also the buffer of the most recent messages. The number of the messages that should be stored is implementation specific, depending on the error management approach that the agent implements; at the very least the agent must store the hash and id of the last received message. +The agent must have a persistent storage to manage the states of known connections and of the client-side information of SMP queues that each connection consists of, and also the buffer of the most recent sent and received messages. The number of the messages that should be stored is implementation specific, depending on the error management approach that the agent implements; at the very least the agent must store the hashes and IDs of the last received and sent messages. ## SMP servers management -SMP agent protocol commands do not contain SMP servers that the agent will use to establish the connections between their users. The servers are part of the agent configuration and can be dynamically added and removed by the agent implementation: +SMP agent protocol commands do not contain the addresses of the SMP servers that the agent will use to create and use the connections (excluding the server address in queue URIs used in JOIN command). The list of the servers is a part of the agent configuration and can be dynamically changed by the agent implementation: - by the client applications via any API that is outside of scope of this protocol. -- by the agents themselves based on servers availability and latency. +- by the agents themselves based on availability and latency of the configured servers. ## SMP agent protocol components SMP agent protocol has 3 main parts: -- the syntax and semantics of messages that SMP agents exchange between each other in order to: - - negotiate establishing unidirectional (simplex) encrypted queues on SMP server(s) +- the syntax and semantics of the messages that SMP agents exchange with each other in order to: + - negotiate establishing unidirectional (simplex) encrypted queues on SMP servers. - exchange client messages and delivery notifications, providing sequential message IDs and message integrity (by including the hash of the previous message). -- the syntax and semantics of the commands (a higher level interface than SMP protocol) that are sent over TCP or other sequential protocol by agent clients to the agents. This protocol allows to create and manage multiple connections, each consisting of two simplex SMP queues. -- the syntax and semantics of the message that the clients of SMP agents should send out-of-band (as pre-shared "invitation" including SMP server, queue ID and encryption key) to ensure [E2E encryption][1] the integrity of SMP queues and protection against active attacks ([MITM attacks][2]). +- the syntax and semantics of the commands that are sent by the agent clients to the agents. This protocol allows to create and manage multiple connections, each consisting of two or more SMP queues. +- the syntax and semantics of the message that the clients of SMP agents should send out-of-band (as pre-shared "invitation" including queue URIs) to protect [E2E encryption][1] from active attacks ([MITM attacks][2]). ## Duplex connection procedure ![Duplex connection procedure](./diagrams/duplex-messaging/duplex-creating.svg) -The procedure of establishing a duplex connection is explained on the example of Alice and Bob creating a bi-directional connection comprised of two unidirectional (simplex) queues, using SMP agents (A and B) to facilitate it, and two different SMP servers (which could be the same server). It is shown on the diagram above and has these steps: +The procedure of establishing a duplex connection is explained on the example of Alice and Bob creating a bi-directional connection consisting of two unidirectional (simplex) queues, using SMP agents (A and B) to facilitate it, and two different SMP servers (which could be the same server). It is shown on the diagram above and has these steps: -1. Alice requests the new connection from the SMP agent A using `NEW` command. -2. Agent A creates an SMP connection on the server (using [SMP protocol](./simplex-messaging.md)) and responds to Alice with the invitation that contains queue information and the encryption key Bob's agent B should use. The invitation format is described in [Connection invitation](#connection-invitation). -3. Alice sends the invitation to Bob via any secure channel they have (out-of-band message). -4. Bob sends `JOIN` command with the invitation as a parameter to agent B to accept the connection. -5. Establishing Alice's SMP connection (with SMP protocol commands): - - Agent B sends an "SMP confirmation" to the SMP queue specified in the invitation - SMP confirmation is an unauthenticated message with an ephemeral key that will be used to authenticate Bob's commands to the queue, as described in SMP protocol, and Bob's info. - - Agent A receives the SMP confirmation containing Bob's key and info. +1. Alice requests the new connection from the SMP agent A using SMP NEW command. +2. Agent A creates an SMP connection on the server (using [SMP protocol](./simplex-messaging.md)) and responds to Alice with the invitation that contains queue information and the encryption key Bob's agent B should use. The invitation format is described in [Connection request](#connection-request). +3. Alice sends the [connection request](#connection-request) to Bob via any secure channel (out-of-band message). +4. Bob sends `JOIN` command with the connection request as a parameter to agent B to accept the connection. +5. Establishing Alice's SMP queue (with SMP protocol commands): + - Agent B sends an "SMP confirmation" with SMP SEND command to the SMP queue specified in the connection request - SMP confirmation is an unauthenticated message with an ephemeral key that will be used to authenticate Bob's commands to the queue, as described in SMP protocol, and Bob's info (profile, public key for E2E encryption, etc.). This message is encrypted using key passed in the connection request (or with the derived key, in which case public key for key derivation should be sent in clear text). + - Agent A receives the SMP confirmation containing Bob's key and info as SMP MSG. - Agent A notifies Alice sending REQ notification with Bob's info. - Alice accepts connection request with ACPT command. - - Agent A secures the queue. + - Agent A secures the queue with SMP KEY command. - Agent B tries sending authenticated SMP SEND command with agent `HELLO` message until it succeeds. Once it succeeds, Bob's agent "knows" the queue is secured. 6. Agent B creates a new SMP queue on the server. 7. Establish Bob's SMP queue: - - Agent B sends `REPLY` message with the invitation to this 2nd queue to Alice's agent (via the 1st queue). - - Agent A, having received this `REPLY` message, sends unauthenticated message to SMP queue with Alice agent's ephemeral key that will be used to authenticate Alice's commands to the queue, as described in SMP protocol, and Alice's info. - - Bob's agent receives the key and Alice's information and secures the queue. + - Agent B sends `REPLY` message (SMP SEND command) with the connection request to this 2nd queue to Alice's agent (via the 1st queue) - this connection request SHOULD use "simplex" URI scheme. + - Agent A, having received `REPLY` message, sends unauthenticated message (SMP SEND) to SMP queue with Alice agent's ephemeral key that will be used to authenticate Alice's commands to the queue, as described in SMP protocol, and Alice's info. + - Bob's agent receives the key and Alice's information and secures the queue (SMP KEY). - Bob's agent sends the notification `INFO` with Alice's information to Bob. - Alice's agent keeps sending `HELLO` message until it succeeds. 8. Agents A and B notify Alice and Bob that connection is established. @@ -97,17 +94,21 @@ The procedure of establishing a duplex connection is explained on the example of At this point the duplex connection between Alice and Bob is established, they can use `SEND` command to send messages. The diagram also shows how the connection status changes for both parties, where the first part is the status of the SMP queue to receive messages, and the second part - the status of the queue to send messages. -The most communication happens between the agents and servers, from the point of view of Alice and Bob they have only 3 steps to do: +The most communication happens between the agents and servers, from the point of view of Alice and Bob there are 4 steps (not including notifications): 1. Alice requests a new connection with `NEW` command and receives the invitation. -2. Alice passes invitation out-of-band to Bob. -3. Bob accepts the connection by sending `JOIN` command with the invitation to his agent. +2. Alice passes connection request out-of-band to Bob. +3. Bob accepts the connection with `JOIN` command with the connection request to his agent. +4. Alice accepts the connection with `ACPT` command. +5. Both parties receive `CON` notification once duplex connection is established. + +Clients SHOULD support establishing duplex connection asynchronously (when parties are intermittently offline) by persisting intermediate states and resuming SMP queue subscriptions. ## Communication between SMP agents -SMP agents communicate via SMP servers managing creation, deletion and operations of SMP queues. +To establish duplex connections and to send messages on behalf of their clients, SMP agents communicate via SMP servers. -Agents can use SMP message client body (the part of the SMP message after header - see [SMP protocol](./simplex-messaging.md)) to transmit agent client messages and exchange messages between each other. +Agents use SMP message client body (the part of the SMP message after header - see [SMP protocol](./simplex-messaging.md)) to transmit agent client messages and exchange messages between each other. Each SMP message client body, once decrypted, contains 3 parts (one of them may include binary message body), as defined by `decryptedSmpMessageBody` syntax: @@ -115,8 +116,9 @@ Each SMP message client body, once decrypted, contains 3 parts (one of them may - `agentMessage` - a command/message to the other SMP agent: - to establish the connection with two SMP queues (`helloMsg`, `replyQueueMsg`) - to send and to acknowledge user messages (`clientMsg`, `acknowledgeMsg`) - - to notify another agent about queue deletion (`deleteQueueMsg`) -- `msgPadding` - an optional message padding to make all SMP messages have consistent size as an additional privacy protection measure. + - to manage SMP queue rotation (`newQueueMessage`, `deleteQueueMsg`) + - to manage encryption key rotation (TODO) +- `msgPadding` - an optional message padding to make all SMP messages have constant size, to prevent servers from observing the actual message size. ### Messages between SMP agents @@ -130,7 +132,9 @@ agentTimestamp = ; RFC3339 previousMsgHash = encoded encoded = -agentMessage = helloMsg / replyQueueMsg / deleteQueueMsg / clientMsg / acknowledgeMsg +agentMessage = helloMsg / replyQueueMsg / + clientMsg / acknowledgeMsg / + newQueueMessage / deleteQueueMsg msgPadding = *OCTET ; optional random bytes to get messages to the same size (as defined in SMP message size) @@ -138,19 +142,18 @@ helloMsg = %s"HELLO" SP signatureVerificationKey [SP %s"NO_ACK"] ; NO_ACK means that acknowledgements to client messages will NOT be sent in this connection by the agent that sent `HELLO` message. signatureVerificationKey = encoded -replyQueueMsg = %s"REPLY" SP ; `queueInfo` is the same as in out-of-band message, see SMP protocol +replyQueueMsg = %s"REPLY" SP connectionRequest ; `connectionRequest` is defined below ; this message can only be sent by the second connection party -deleteQueueMsg = %s"DEL" ; notification that recipient queue will be deleted -; no need to notify the other party about suspending queue separately, as suspended and deleted queues are the same to the sender -; NOT SUPPORTED with the current implementation - clientMsg = %s"MSG" SP size CRLF clientMsgBody CRLF ; CRLF is in addition to CRLF in decryptedSmpMessageBody size = 1*DIGIT clientMsgBody = *OCTET -acknowledgeMsg = %s"ACK" SP agentMsgId SP ackStatus -; NOT SUPPORTED with the current implementation +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 @@ -160,26 +163,47 @@ ackErrorType = ackUnknownMsg / ackProhibitedMsg / ackSyntaxErr ackUnknownMsg = %s"UNKNOWN" -ackProhibitedMsg = %s"PROHIBITED" ; e.g. "HELLO" or "REPLY" +ackProhibitedMsg = %s"PROHIBITED" ; unexpected message e.g. "HELLO" or "REPLY" ackSyntaxErr = %s"SYNTAX" SP syntaxErrCode syntaxErrCode = 1*DIGIT ; TODO + +newQueueMsg = %s"NEW" SP 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 +; 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 ``` #### HELLO message -This is the first message that both agents send after the respective SMP queue is secured by the receiving agent (see diagram). It contains the verification key that the sender will use to cryptographically sign the messages. +This is the first message that both agents send after the respective SMP queue is secured by the receiving agent (see diagram). It MAY contain the public key that the recipient would use to verify messages signed by the sender. -Sending agent might need to retry sending HELLO message, as it would not have any other confirmation that the queue is secured other than the success of sending this message with the signed SEND command of SMP protocol. +Sending agent might need to retry sending HELLO message, as it would not have any other confirmation that the queue is secured other than the success of sending this message with the signed SMP SEND command. #### REPLY message -This is the message that is sent by the agent that received an out-of-band invitation to pass the invitation to the reply SMP queue to the agent that originated the connection (see diagram). +This is the message that is sent by the agent that received an out-of-band connection request to pass the connection request for the reply SMP queues to the agent that originated the connection (see diagram). #### MSG message This is the agent envelope used to send client messages once the connection is established. Do not confuse it with the MSG response from SMP server to the agent and MSG response from SMP agent to the client that are sent in different contexts. +#### ACK message + +This message is sent to confirm the client message reception. It includes received message number, message hash and the reception status. + +#### NEW message + +This message is sent to add an additional SMP queue to the connection. Unlike REPLY message it can be sent at any time. + +#### DEL message + +This message is sent to notify that the queue with passed URI will be deleted - having received this message, the receiving agent should no longer send messages to this queue. In case it was the only queue in the connection to which the agent could send the messages, it MAY also delete the reply queue(s) in the connection. + ## SMP agent commands This part describes the transmissions between users and client-side SMP agents: commands that the users send to create and operate duplex connections and SMP agent responses and messages they deliver. @@ -204,7 +228,7 @@ agentMsg = invitation / connRequest / connInfo / connected / unsubscribed / conn newCmd = %s"NEW" [SP %s"NO_ACK"] ; response is `invitation` or `error` ; NO_ACK parameter currently not supported -invitation = %s"INV" SP ; `queueInfo` is the same as in out-of-band message, see SMP protocol +invitation = %s"INV" SP ; `connectionRequest` is defined below connRequest = %s"REQ" SP confirmationId SP msgBody ; msgBody here is any binary information identifying connection request @@ -231,8 +255,8 @@ connDown = %s"DOWN" connUp = %s"UP" ; restored connection -joinCmd = %s"JOIN" SP [SP %s"NO_REPLY"] [SP %s"NO_ACK"] -; `queueInfo` is the same as in out-of-band message, see SMP protocol +joinCmd = %s"JOIN" SP [SP %s"NO_REPLY"] [SP %s"NO_ACK"] +; `connectionRequest` is defined below ; response is `connected` or `error` suspendCmd = %s"OFF" ; can be sent by either party, response `ok` or `error` @@ -275,9 +299,12 @@ previousMsgId = agentMsgId acknowledgeCmd = %s"ACK" SP agentMsgId ; ID assigned by receiving agent (in MSG "R") -received = %s"RCVD" SP agentMsgId ; ID assigned by sending agent (in SENT response) +received = %s"RCVD" SP agentMsgId SP msgIntegrity +; ID assigned by sending agent (in SENT response) ; currently not implemented +msgStatus = ok | error + ok = %s"OK" error = %s"ERR" SP @@ -287,13 +314,13 @@ error = %s"ERR" SP #### NEW command and INV response -`NEW` command is used to create a connection and an invitation to be sent out-of-band to another protocol user (the joining party). It should be used by the client of the agent that initiates creating a duplex connection (the initiating party). +`NEW` command is used to create a connection and a connection request to be sent out-of-band to another protocol user (the joining party). It should be used by the client of the agent that initiates creating a duplex connection (the initiating party). `INV` response is sent by the agent to the client of the initiating party. #### JOIN command -It is used to create a connection and accept the invitation received out-of-band. It should be used by the client of the agent that accepts the connection (the joining party). +It is used to create a connection and accept the connection request received out-of-band. It should be used by the client of the agent that accepts the connection (the joining party). #### REQ notification and ACPT command @@ -311,17 +338,17 @@ Once the connection is established and ready to accept client messages, both age This command can be used by the client to resume receiving messages from the connection that was created in another TCP/client session. Agent response to this command can be `OK` or `ERR` in case connection does not exist (or can only be used to send connections - e.g. when the reply queue was not created). -#### SEND command and MID, SENT and MERR responses +#### SEND command and MID, SENT, RCVD and MERR responses `SEND` command is used by the client to send messages. -`MID` notification with the message ID (the sequential message number that includes both sent and received messages in the connection) is sent to the client to confirm that the message is accepted by the agent, before it is sent to the SMP server. +`MID` response with the message ID (the sequential message number that includes both sent and received messages in the connection) is sent to the client to confirm that the message is accepted by the agent, before it is sent to the SMP server. -`SENT` response is sent by the agent to confirm that the message was delivered to the SMP server. This notification contains the same message ID as `MID` notification. `SENT` notification, depending on network availability, can be sent at any time later, potentially in the next client session. +`SENT` notification is sent by the agent to confirm that the message was delivered to at least one of SMP servers. This notification contains the same message ID as `MID` notification. `SENT` notification, depending on network availability, can be sent at any time later, potentially in the next client session. -In case of the failure to send the message for any other reason than network connection or message queue quota - e.g. authentication error (`ERR AUTH`) or syntax error (`ERR CMD error`), the agent will send to the client `MERR` notification with the message ID, and this message delivery will no longer be attempted. +`RCVD` notification is sent by the agent when it receives `ACK` message from the receiving agent. This notification contains reception status, only one successful notification will be sent, and multiple error notifications will be sent in case `ACK` had error status. -In case of client disconnecting from the agent, the pending messages will not be sent until the client re-connects to the agent and subscribes to the connection that has pending messages. +In case of the failure to send the message for any other reason than network connection or message queue quota - e.g. authentication error (`ERR AUTH`) or syntax error (`ERR CMD error`), the agent will send to the client `MERR` notification with the message ID, and this message delivery will no longer be attempted to this SMP queue. #### MSG notification @@ -348,11 +375,34 @@ It is used to suspend the receiving SMP queue - sender will no longer be able to It is used to delete the connection and all messages in it, as well as the receiving SMP queue and all messages in it that were remaining on the server. Agent response to this command can be `OK` or `ERR`. This command is irreversible. -## Connection invitation +## Connection request -Connection invitation `queueInfo` is generated by SMP agent in response to `newCmd` command (`"NEW"`), used by another party user with `joinCmd` command (`"JOIN"`), and then another invitation is sent by the agent in `replyQueueMsg` and used by the first party agent to connect to the reply queue (the second part of the process is invisible to the users). +Connection request `connectionRequest` is generated by SMP agent in response to `newCmd` command (`"NEW"`), used by another party user with `joinCmd` command (`"JOIN"`), and then another connection request is sent by the agent in `replyQueueMsg` and used by the first party agent to connect to the reply queue (the second part of the process is invisible to the users). -See SMP protocol [out-of-band messages](./simplex-messaging.md#out-of-band-messages) for connection invitation syntax. +Connection request syntax: + +``` +connectionRequest = connectionProtocol "/" action "#/?smp=" smpQueues "&e2e=" e2eEncryption +action = %s"connect" +connectionProtocol = (%s"https://" clientAppServer) | %s"simplex:" +clientAppServer = hostname [ ":" port ] +; client app server, e.g. simplex.chat +e2eEncryption = encryptionScheme ":" publicKey +encryptionScheme = %s"rsa" ; end-to-end encryption and key exchange protocols, + ; the current hybrid encryption scheme (RSA-OAEP/AES-256-GCM-SHA256) + ; will be replaced with double ratchet protocol and DH key exchange. +publicKey = +smpQueues = smpQueue [ "," 1*smpQueue ] ; SMP queues for the connection +smpQueue = +``` + +All parameters are passed via URI hash to avoid sending them to the server (in case "https" scheme is used) - they can be used by the client-side code and processed by the client application. Parameters `smp` and `e2e` can be present in any order, any unknown additional parameters SHOULD be ignored. + +`clientAppServer` is not an SMP server - it is a server that shows the instruction on how to download the client app that will connect using this connection request. This server can also host a mobile or desktop app manifest so that this link is opened directly in the app if it is installed on the device. + +"simplex" URI scheme in `connectionProtocol` can be used instead of client app server, to connect without creating any web traffic. Client apps MUST support this URI scheme. + +See SMP protocol [out-of-band messages](./simplex-messaging.md#out-of-band-messages) for syntax of `queueURI`. [1]: https://en.wikipedia.org/wiki/End-to-end_encryption [2]: https://en.wikipedia.org/wiki/Man-in-the-middle_attack diff --git a/protocol/simplex-messaging.md b/protocol/simplex-messaging.md index 884ae98ec..1ae222b1d 100644 --- a/protocol/simplex-messaging.md +++ b/protocol/simplex-messaging.md @@ -7,11 +7,13 @@ - [SMP Model](#smp-model) - [Out-of-band messages](#out-of-band-messages) - [Simplex queue](#simplex-queue) +- [SMP queue URI](#smp-queue-uri) - [SMP procedure](#smp-procedure) - [SMP qualities and features](#smp-qualities-and-features) - [Cryptographic algorithms](#cryptographic-algorithms) - [Simplex queue IDs](#simplex-queue-ids) -- [Server privacy requirements](#server-privacy-requirements) +- [Server security requirements](#server-security-requirements) +- [Message delivery notifications](#message-delivery-notifications) - [SMP commands](#smp-commands) - [Correlating responses with commands](#correlating-responses-with-commands) - [Command authentication](#command-authentication) @@ -20,14 +22,19 @@ - [Create queue command](#create-queue-command) - [Subscribe to queue](#subscribe-to-queue) - [Secure queue command](#secure-queue-command) + - [Enable notifications command](#enable-notifications-command) - [Acknowledge message delivery](#acknowledge-message-delivery) - [Suspend queue](#suspend-queue) - [Delete queue](#delete-queue) - [Sender commands](#sender-commands) - [Send message](#send-message) + - [Notifier commands](#notifier-commands) + - [Subscribe to queue notifications](#subscribe-to-queue-notifications) - [Server messages](#server-messages) - [Queue IDs response](#queue-ids-response) - [Deliver queue message](#deliver-queue-message) + - [Notifier queue ID response](#notifier-queue-id-response) + - [Deliver message notification](#deliver-message-notification) - [Subscription END notification](#subscription-end-notification) - [Error responses](#error-responses) - [OK response](#ok-response) @@ -66,37 +73,15 @@ The SMP model has three communication participants: the recipient, the message b SMP server manages multiple "simplex queues" - data records on the server that identify communication channels from the senders to the recipients. The same communicating party that is the sender in one queue, can be the recipient in another - without exposing this fact to the server. -The queue record consists of 2 unique random IDs generated by the server, one for the recipient and another for the sender, and 2 keys to authenticate the recipient and the sender respectively, provided by the client. The users of SMP protocol must use a unique key for each queue, to avoid the possibility of aggregating and analysing their queues in case SMP server is compromised. +The queue record consists of 2 unique random IDs generated by the server, one for the recipient and another for the sender, and 2 keys to authenticate the recipient and the sender respectively, provided by the client. The users of SMP protocol must use a unique key for each queue, to avoid the possibility of aggregating and analyzing their queues in case SMP server is compromised. Creating and using the queue requires sending commands to the SMP server from the recipient and the sender - they are described in detail in [SMP commands](#smp-commands) section. ## Out-of-band messages -The out-of-band message with the queue information is sent via some trusted alternative channel from the recipient to the sender. This message is used to share the encryption (a.k.a. "public") key that the sender will use to encrypt the messages (to be decrypted by the recipient), sender queue ID, server hostname and any other information necessary to establish secure encrypted connection with SMP server (see [Appendix A](#appendix-a) for SMP transport protocol). +The out-of-band message with the queue information is sent via some trusted alternative channel from the recipient to the sender. This message is used to share one or several [queue URIs](#smp-queue-uri) that parties can use to establish the initial connection, the encryption scheme and, it can include the public key(s) for end-to-end encryption. -The [ABNF][8] syntax of the message is: - -```abnf -queueInfo = %s"smp::" smpServer "::" queueId "::" encryptionKey -smpServer = srvHost [":" port] ["#" serverKeyHash] -srvHost = ; RFC1123, RFC5891 -port = 1*DIGIT -serverKeyHash = encoded -queueId = encoded -encryptionKey = %s"rsa:" x509encoded ; the recipient's RSA public key for sender to encrypt messages -x509encoded = -encoded = -``` - -`hostname` can be IP address or domain name, as defined in RFC 1123, section 2.1. - -`port` is optional, the default TCP port for SMP protocol is 5223. - -`serverKeyHash` is an optional hash of the server transport key used during transport handshake (see [Appendix A](#appendix-a)). - -Encryption keys are encoded using [X509][11] specification. - -Defining the approach to out-of-band message passing is out of scope of this protocol. +The approach to out-of-band message passing and their syntax should be defined in application-level protocols. ## Simplex queue @@ -124,6 +109,29 @@ Queue is defined by recipient ID `RID` and sender ID `SID`, unique for the serve The protocol uses different IDs for sender and recipient in order to provide an additional privacy by preventing the correlation of senders and recipients commands sent over the network - in case the encrypted transport is compromised, it would still be difficult to correlate senders and recipients without access to the queue records on the server. +## SMP queue URI + +The SMP queue URIs MUST include server identity, queue hostname, an optional port, sender queue ID and the public key that the clients must use to verify responses. Server identity is used to establish secure connection protected from MITM attack with SMP server (see [Appendix A](#appendix-a) for SMP transport protocol). + +The [ABNF][8] syntax of the queue URI is: + +```abnf +queueURI = %s"smp://" smpServer "/" queueId "#" serverSignaturePublicKey +; serverSignaturePublicKey syntax is defined below +smpServer = serverIdentity "@" srvHost [":" port] +srvHost = ; RFC1123, RFC5891 +port = 1*DIGIT +serverIdentity = base64url +queueId = base64url +base64url = ; RFC4648, section 5 +``` + +`hostname` can be IP address or domain name, as defined in RFC 1123, section 2.1. + +`port` is optional, the default TCP port for SMP protocol is 5223. + +`serverIdentity` is a required hash of the server certificate SPKI block (without line breaks, header and footer) used by the client to validate server certificate during transport handshake (see [Appendix A](#appendix-a)) + ## SMP procedure The SMP procedure of creating a simplex queue on SMP server is explained using participants Alice (the recipient) who wants to receive messages from Bob (the sender). @@ -266,7 +274,7 @@ Simplex Messaging Protocol: - One unique "public" key is used by the servers to authenticate requests to send the messages into the queue, and another unique "public" key - to retrieve the messages from the queue. "Unique" here means that each "public" key is used only for one queue and is not used for any other context - effectively, this key is not public and does not represent any participant identity. - - Both recipient and sender "public" keys are provided to the server by the queue recipient. "Public" key `RK` is provided when the queue is created, public key `SK` is proviced when the queue is secured. + - Both recipient and sender "public" keys are provided to the server by the queue recipient. "Public" key `RK` is provided when the queue is created, public key `SK` is provided when the queue is secured. - The "public" keys known to the server and used to authenticate commands from the participants are unrelated to the keys used to encrypt and decrypt the messages - the latter keys are also unique per each queue but they are only known to participants, not to the servers. @@ -274,29 +282,36 @@ Simplex Messaging Protocol: ## Cryptographic algorithms -Simplex messaging clients need to cryptographically sign commands for the following operations: +Simplex messaging clients and servers must cryptographically sign commands, responses and messages for the following operations: - With the recipient's key `RK` (server to verify): - create the queue (`NEW`) - subscribe to queue (`SUB`) - secure the queue (`KEY`) + - enable queue notifications (`NKEY`) - acknowledge received messages (`ACK`) - suspend the queue (`OFF`) - delete the queue (`DEL`) - With the sender's key `SK` (server to verify): - send messages (`SEND`) +- With the optional notifier's key: + - subscribe to message notifications (`NSUB`) +- With the server's key (for recipient and sender to verify) + - queue IDs response (`IDS`) + - notifier queue ID response (`NID`) + - delivered messages (`MSG`) + - `OK` and `ERR` responses (excluding error responses not related to a queue) -To sign and verify commands, clients and servers MUST use RSA-PSS algorithm defined in [RFC3447][2]. +To sign/verify transmissions clients and servers MUST use Ed25519 or Ed448 algorithm defined in [RFC8709][15]. -To optionally sign and verify messages, clients SHOULD use RSA-PSS algorithm. +To encrypt/decrypt message bodies delivered to the recipients, servers/clients MUST use x25519 or x448 algorithm defined in [RFC8709][15] to derive the shared secret (TODO encryption scheme). -To encrypt and decrypt messages, clients and servers SHOULD use RSA-OAEP algorithm defined in [RFC3447][2]. +Clients MUST encrypt message bodies sent via SMP servers - the protocol for this end-to-end encryption should be chosen by the clients using SMP protocol. The reasons to use these algorithms: -- They are supported by WebCrypto API. -- They are more widely supported than ECC algorithms. -- They are newer versions than RSA-PKCS1-v1_5 encryption and signature schemes. +- Faster operation than RSA algorithms. +- DH key exchange provides forward secrecy. Future versions of the protocol may allow different cryptographic algorithms. @@ -319,6 +334,18 @@ Simplex messaging server implementations MUST NOT create, store or send to any o - Any other information that may compromise privacy or [forward secrecy][4] of communication between clients using simplex messaging servers. +## Message delivery notifications + +Supporting message delivery while the client mobile app is not running requires sending push notifications with the device token. All alternative mechanisms for background message delivery are unreliable, particularly on iOS platform. Obviously, supporting push notification delivery by simply subscribing to messages would reduce meta-data privacy as it allows to see all queues that a given device uses. + +To protect the privacy of the recipients, there are several commands in SMP protocol that allow enabling and subscribing to message notifications from SMP queues, using separate set of "notifier keys" and via separate queue IDs - as long as SMP server is not compromised, these notifier queue IDs cannot be correlated with recipient or sender queue IDs. + +The clients can optionally instruct a dedicated push notification server to subscribe to notifications and deliver push notifications to the device, which can then retrieve the messages in the background and send local notifications to the user - this is out of scope of SMP protocol. The commands that SMP protocol provides to allow it: + +- `enableNotifications` (`"NKEY"`) with `notifierId` (`"NID"`) response - see [Enable notifications command](#enable-notifications-command). +- `subscribeNotifications` (`"NSUB"`) - see [Subscribe to queue notifications](#subscribe-to-queue-notifications). +- `messageNotification` (`"NMSG"`) - see [Deliver message notification](#deliver-message-notification). + ## SMP commands Commands syntax below is provided using [ABNF][8] with [case-sensitive strings extension][8a]. @@ -327,14 +354,18 @@ Each transmission between the client and the server must have this format/syntax ```abnf transmission = [signature] SP signed SP pad ; pad to the fixed block size -signed = [corrId] SP [queueId] SP cmd -cmd = ping / recipientCmd / send / serverMsg -recipientCmd = create / subscribe / secure / acknowledge / suspend / delete -serverMsg = pong / queueIds / message / unsubscribed / ok / error +signed = sessionIdentifier SP [corrId] SP [queueId] SP cmd ; corrId is required in client commands and server responses, + ; corrId is empty in server notifications. +cmd = ping / recipientCmd / send / subscribeNotifications / serverMsg +recipientCmd = create / subscribe / secure / enableNotifications / + acknowledge / suspend / delete +serverMsg = queueIds / message / notifierId / messageNotification / + unsubscribed / ok / error corrId = 1*(%x21-7F) ; any characters other than control/whitespace queueId = encoded ; empty queue ID is used with "create" command signature = encoded -; empty signature can be used with "create", "send" and "ping" commands and server messages +; empty signature can be used with "send" before the queue is secured with secure command +; signature is always empty with "ping" and "serverMsg" encoded = ``` @@ -344,23 +375,24 @@ The syntax of specific commands and responses is defined below. ### Correlating responses with commands -The server should send `queueIds`, `error` and `ok` responses in the same order within each queue ID as the commands received in the transport connection, so that they can be correlated by the clients. To simplify correlation of commands and responses, the server should use the same `corrId` in the response as in the command sent by the client. +The server should send `queueIds`, `error` and `ok` responses in the same order within each queue ID as the commands received in the transport connection, so that they can be correlated by the clients. To simplify correlation of commands and responses, the server must use the same `corrId` in the response as in the command sent by the client. If the transport connection is closed before some responses are sent, these responses should be discarded. ### Command authentication -SMP servers must authenticate all transmissions (excluding `ping` and `send` commands) by verifying the provided signatures. Command signature should be generated by applying RSA-PSS algorithm to the `signed` block of the transmission using the key associated with the queue ID (sender's or recipient's, depending on which queue ID is used). +SMP servers must authenticate all transmissions (excluding `ping` and initial `send` commands) by verifying the client signatures. Command signature should be generated by applying the algorithm specified for the queue to the `signed` block of the transmission, using the key associated with the queue ID (recipient's, sender's or notifier's, depending on which queue ID is used). ### Keep-alive command -To keep the transport connection alive and to generate noise traffic the clients should use `ping` command to which the server responds with `pong` response. This command should be sent unsigned and without queue ID. +To keep the transport connection alive and to generate noise traffic the clients should use `ping` command to which the server responds with `ok` response. This command should be sent unsigned and without queue ID. ```abnf ping = %s"PING" -pong = %s"PONG" ``` +This command is always send unsigned. + ### Recipient commands Sending any of the commands in this section (other than `create`, that is sent without queue ID) is only allowed with recipient's ID (`RID`). If sender's ID is used the server must respond with `"ERR AUTH"` response (see [Error responses](#error-responses)). @@ -370,24 +402,45 @@ Sending any of the commands in this section (other than `create`, that is sent w This command is sent by the recipient to the SMP server to create a new queue. The syntax is: ```abnf -create = %s"NEW" SP recipientKey -recipientKey = %s"rsa:" x509encoded ; the recipient's RSA public key for this queue +create = %s"NEW" SP recipientSignaturePublicKey SP recipientDhPublicKey +recipientSignaturePublicKey = signaturePublicKey +; the recipient's public key to verify commands for this queue + +signaturePublicKey = signatureScheme ":" x509encoded +signatureScheme = %s"rsa" | %s"ed25519" | %s"ed448" +; "rsa" means deprecated RSA-PSS signature scheme, +; it must not be used for the new queues. + +recipientDhPublicKey = dhPublicKey +dhPublicKey = encryptionScheme ":" x509encoded +; the recipient's key for DH exchange to derive the secret +; that the server will use to encrypt delivered message bodies + +encryptionScheme = %s"x25519" | %s"x448" +; TODO change to define the encryption scheme, e.g. "crypto_box" + x509encoded = ``` -If the queue is created successfully, the server must send `queueIds` response with the recipient's and sender's queue IDs: +If the queue is created successfully, the server must send `queueIds` response with the recipient's and sender's queue IDs and public keys to sign all responses and messages and to encrypt delivered message bodies: ```abnf queueIds = %s"IDS" SP recipientId SP senderId + SP serverSignaturePublicKey SP serverDhPublicKey +serverSignaturePublicKey = signatureKey +; the server's public key to verify responses and messages for this queue +serverDhPublicKey = dhPublicKey +; the server's key for DH exchange to derive the secret +; that the server will use to encrypt delivered message bodies to the recipient recipientId = encoded senderId = encoded ``` -This response should be sent with empty queue ID (the second part of the transmission). - Once the queue is created, the recipient gets automatically subscribed to receive the messages from that queue, until the transport connection is closed. The `subscribe` command is needed only to start receiving the messages from the existing queue when the new transport connection is opened. -NEW `transmission` must be signed using the `recipientKey` that was passed in the transmission. +`NEW` transmission MUST be signed using the private part of the `recipientSignaturePublicKey` – this verifies that the client has the private key that will be used to sign subsequent commands for this queue. + +`IDS` response transmission MUST be sent signed with `serverSignaturePublicKey` – this verifies that the server has the private key that will be used to sign subsequent responses and messages for this queue. This response should be sent with empty queue ID (the third part of the transmission). #### Subscribe to queue @@ -401,19 +454,42 @@ If subscription is successful the server must respond with the first available m The first message will be delivered either immediately or as soon as it is available; to receive the following message the recipient must acknowledge the reception of the message (see [Acknowledge message delivery](#acknowledge-message-delivery)). +This transmission and its response MUST be signed. + #### Secure queue command This command is sent by the recipient to the server to add sender's key to the queue: ```abnf -secure = %s"KEY" SP senderKey -senderKey = %s"rsa:" x509encoded ; the sender's RSA public key for this queue +secure = %s"KEY" SP senderSignaturePublicKey +senderSignaturePublicKey = signaturePublicKey +; the sender's key to verify SEND commands for this queue ``` `senderKey` is received from the sender as part of the first message - see [Send Message](#send-message) command. Once the queue is secured only signed messages can be sent to it. +#### Enable notifications command + +This command is sent by the recipient to the server to add notifier's key to the queue, to allow push notifications server to receive notifications when the message arrives, via a separate queue ID, without receiving message content. + +```abnf +enableNotifications = %s"NKEY" SP notifierKey +notifierKey = signatureScheme ":" x509encoded ; the notifier's public key public key to verify NSUB command for this queue +``` + +The server will respond with `notifierId` response if notifications were enabled and the notifier's key was successfully added to the queue: + +```abnf +notifierId = %s"NID" SP notifierId +recipientId = encoded +``` + +This response is sent with the recipient's queue ID (the third part of the transmission). + +To receive the message notifications, `subscribeNotifications` command ("NSUB") must be sent signed with the notifier's key. + #### Acknowledge message delivery The recipient should send the acknowledgement of message delivery once the message was stored in the client, to notify the server that the message should be deleted: @@ -485,12 +561,26 @@ The body should be encrypted with the recipient's "public" key (`EK`); once decr decryptedBody = [clientHeader] CRLF clientBody CRLF clientHeader = senderKeyMsg senderKeyMsg = %s"KEY" SP senderKey -senderKey = %s"rsa:" x509encoded ; the sender's RSA public key for this queue +senderKey = signatureScheme ":" x509encoded ; the sender's public key to sign SEND commands for this queue clientBody = *OCTET ``` `clientHeader` in the initial unsigned message is used to transmit sender's server key and can be used in the future revisions of SMP protocol for other purposes. +### Notifier commands + +#### Subscribe to queue notifications + +The push notifications server (notifier) must use this command to start receiving message notifications from the queue: + +```abnf +subscribeNotifications = %s"NSUB" +``` + +If subscription is successful the server must respond with `ok` response if no messages are available. The notifier will be receiving the message notifications from this queue until the transport connection is closed or until another transport connection subscribes to notifications from the same simplex queue - in this case the first subscription should be cancelled and [subscription END notification](#subscription-end-notification) delivered. + +The first message notification will be delivered either immediately or as soon as the message is available. + ### Server messages #### Queue IDs response @@ -504,7 +594,9 @@ See its syntax in [Create queue command](#create-queue-command) The server must deliver messages to all subscribed simplex queues on the currently open transport connection. The syntax for the message delivery is: ```abnf -message = %s"MSG" SP msgId SP timestamp SP size SP msgBody SP +message = %s"MSG" SP encryptedMessage +encryptedMessage = +sentMessage = msgId SP timestamp SP size SP msgBody SP msgId = encoded timestamp = ``` @@ -513,7 +605,27 @@ timestamp = `timestamp` - the UTC time when the server received the message from the sender, must be in date-time format defined by [RFC 3339][10] -`binaryMsg` - see syntax in [Send message](#send-message) +`msgBody` - see syntax in [Send message](#send-message) + +When server delivers the messages to the recipient, message body should be encrypted with the secret derived from DH exchange using the keys passed during the queue creation and returned with `queueIds` response. + +This is done to prevent the possibility of correlation of incoming and outgoing traffic of SMP server inside transport protocol. + +#### Notifier queue ID response + +Server must respond with this message when queue notifications are enabled. + +See its syntax in [Enable notifications command](#enable-notifications-command) + +#### Deliver message notification + +The server must deliver message notifications to all simplex queues that were subscribed with `subscribeNotifications` command ("NSUB") on the currently open transport connection. The syntax for the message notification delivery is: + +```abnf +messageNotification = %s"NMSG" +``` + +Message notification does not contain any message data or meta-data, it only notifies that the message is available. #### Subscription END notification @@ -566,52 +678,47 @@ ok = %s"OK" Both the recipient and the sender can use TCP or some other, possibly higher level, transport protocol to communicate with the server. The default TCP port for SMP server is 5223. -By default, the client and server should use the protocol presented below, that does not depend on a centralized certificate authority. +For scenarios when meta-data privacy is critical, it is recommended that clients: +- communicating over Tor network, +- establish a separate connection for each SMP queue, +- send noise traffic (using PING command). -Transport is encrypted with [AEAD-GCM][12] protocol with two random symmetric AES 256-bit keys and two random base IVs that will be agreed during the handshake. Both client and the server should maintain two 32-bit word counters, one for the sent and one for the received messages. The IV for each message should be computed by xor-ing the sequential message counter, starting from 0, with the first 32 bits of agreed base IV (the number is encoded in network byte order). +In addition to that, the servers can be deployed as Tor onion services. -To establish the session keys and base IVs, the server should have an asymmetric key pair generated during server deployment and unknown to the clients. The users should know the key hash (256 bits) in advance in order to be able to validate the server public key during transport connection handshake. +The transport protocol should provide the following: +- server authentication (by matching server certificate hash with `serverIdentity`), +- forward secrecy (by encrypting the traffic using ephemeral keys agreed during transport handshake), +- integrity (preventing data modification by the attacker without detection), +- unique channel binding (`sessionIdentifier`) to include in the signed part of SMP transmissions. -The handshake sequence is the following: +By default, the client and server communicate using [TLS 1.3 protocol][13] restricted to: +- TLS_AES_256_GCM_SHA384 cypher suite, +- ed25519 and ed448 EdDSA algorithms for signatures, +- x25519 and x448 ECDHE groups for key exchange. +- servers must send only one self-signed certificate in the handshake, clients must abort the connection in case more than one certificate is sent. +- server and client TLS configuration should not allow resuming the sessions. -1. Once the connection is established, the server sends the `server_header` followed by its public RSA key encoded in X509 binary (not base-64 encoded) format to the client. -2. The client compares the SHA256 hash of the received key with the hash it already has (e.g. received as part of connection invitation or as SMP server configuration). If the hash does not match, the client must terminate the connection. -3. If the hash is the same, the client should generate two random symmetric 256-bit AES keys and two base IVs that will be used as session keys/IVs by the client and the server. -4. The client then should create the `client_handshake` block and send it to the server, encrypted using [RSA-OAEP][2] scheme with the server public key: `rsa-encrypt(client_handshake)`. `snd_aes_key` and `snd_base_iv` will be used by the client to encrypt **sent** messages and by the server to decrypt them, `rcv_aes_key` and `rcv_base_iv` will be used by the client to decrypt **received** messages and by the server to encrypt them. `client_handshake` also contains `block_size` and reserved `protocol` blocks (see syntax). -5. The server should decrypt the received AES keys and base IVs with its private RSA key. -6. In case of successful decryption, the server should send encrypted welcome block (`encrypted_welcome_block`) that contains SMP protocol version supported by the server. +During TLS handshake the client must validate that the hash of the server certificate SPKI block is equal to the `serverIdentity` the client received as part of SMP server address; if the server identity does not match the client must abort the connection. -All the subsequent data, both from the client and from the server, should be sent padded to the fixed agreed block size, encrypted with symmetric AES keys and base IVs (incremented by counters on both sides), that were sent by the client during the handshake. If the application needs to transmit a larger message, it should be broken down into fragments. +Once TLS handshake is complete, client and server will exchange blocks of fixed size (16384 bytes). -Handshake blocks sent by the client and the server have this syntax: +The first block sent by the client should be `clientHello` and the server should respond with `serverHello`: ```abnf -server_header = block_size protocol key_size -block_size = 4*4(OCTET) ; 4-byte block size sent by the server -protocol = 2*2(%x00) ; 0, reserved -key_size = 2*2(OCTET) ; the size of the encoded key in bytes (binary encoded in X509 standard) +clientHello = SP smpVersion SP reserved pad +serverHello = sessionIdentifier SP smpVersion SP reserved pad +sessionIdentifier = ; unique session identifier derived from transport connection handshake + ; it should be included in all SMP transmissions sent in this transport connection. -client_handshake = client_block_size protocol snd_aes_key snd_base_iv rcv_aes_key rcv_base_iv -client_block_size = 4*4(OCTET) ; 4-byte block size sent by the client, -; to confirm or override the block size sent by the server -client_protocol = 2*2(%x00) ; 0, reserved -snd_aes_key = 32*32(OCTET) -snd_base_iv = 16*16(OCTET) -rcv_aes_key = 32*32(OCTET) -rcv_base_iv = 16*16(OCTET) - -transport_block = aes_body_auth_tag aes_encrypted_body -; size is sent by server during handshake, usually 4096 bytes -aes_body_auth_tag = 16*16(OCTET) -aes_encrypted_body = 1*OCTET - -encrypted_welcome_block = transport_block -welcome_block = smp_version SP pad ; decrypt(encrypted_welcome_block) -smp_version = %s"v" 1*DIGIT "." 1*DIGIT "." 1*DIGIT ["-" 1*ALPHA "." 1*DIGIT] ; in semver format - ; for example: v123.456.789-alpha.7 +smpVersion = %s"SMP v" 1*DIGIT "." 1*DIGIT "." 1*DIGIT ; semver format, the version in this document is v0.5.0 +reserved = pad = 1*OCTET ``` +For TLS 1.3 transport client should assert that `sessionIdentifier` is equal to `tls-unique` channel binding defined in [RFC 5929][14] (TLS Finished message struct); we pass it in `serverHello` block to allow communication over some other transport protocol. + +The communication party (client or server) that has the lower protocol version should assume that this version will be supported by another party, the party with the higher protocol version should abort the connection in case they cannot support the lower version. + [1]: https://en.wikipedia.org/wiki/Man-in-the-middle_attack [2]: https://en.wikipedia.org/wiki/End-to-end_encryption [3]: https://en.wikipedia.org/wiki/QR_code @@ -624,3 +731,6 @@ pad = 1*OCTET [10]: https://tools.ietf.org/html/rfc3339 [11]: https://tools.ietf.org/html/rfc5280 [12]: https://tools.ietf.org/html/rfc7714 +[13]: https://datatracker.ietf.org/doc/html/rfc8446 +[14]: https://datatracker.ietf.org/doc/html/rfc5929#section-3 +[15]: https://www.rfc-editor.org/rfc/rfc8709.html From 04c65d98da850fcc0903dc9729a13b4705a9d001 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 4 Dec 2021 18:42:08 +0000 Subject: [PATCH 12/20] support permanent connection link ("contact") in SMP agent protocols (#143) * open/public queue extension for SMP and SMP agent protocols * add connection mode - invitation or contact * use ConnectionMode with REQ and ACPT agent notification/command * parameterize ConnectionRequest with ConnectionMode * implement Contact connection mode for permanent connection links * tests for contact connections --- migrations/20210101_initial.sql | 2 +- migrations/20211202_connection_mode.sql | 11 + protocol/agent-protocol.md | 38 +++- rfcs/2021-05-17-open-connection.md | 52 +++++ rfcs/{ => done}/2021-01-20-logging.md | 0 rfcs/{ => done}/2021-01-26-crypto.md | 0 rfcs/{ => done}/2021-01-26-messages.md | 0 rfcs/{ => rejected}/2021-05-23-broadcast.md | 0 rfcs/{ => rejected}/2021-05-23-groups2.md | 0 .../{ => rejected}/2021-05-23-introduction.md | 0 simplexmq.cabal | 3 +- src/Simplex/Messaging/Agent.hs | 81 +++++-- src/Simplex/Messaging/Agent/Client.hs | 27 +++ src/Simplex/Messaging/Agent/Protocol.hs | 208 ++++++++++++++---- src/Simplex/Messaging/Agent/Store.hs | 32 ++- src/Simplex/Messaging/Agent/Store/SQLite.hs | 97 ++++++-- tests/AgentTests.hs | 89 ++++++-- tests/AgentTests/ConnectionRequestTests.hs | 25 +-- tests/AgentTests/FunctionalAPITests.hs | 33 ++- tests/AgentTests/SQLiteTests.hs | 28 +-- 20 files changed, 575 insertions(+), 151 deletions(-) create mode 100644 migrations/20211202_connection_mode.sql create mode 100644 rfcs/2021-05-17-open-connection.md rename rfcs/{ => done}/2021-01-20-logging.md (100%) rename rfcs/{ => done}/2021-01-26-crypto.md (100%) rename rfcs/{ => done}/2021-01-26-messages.md (100%) rename rfcs/{ => rejected}/2021-05-23-broadcast.md (100%) rename rfcs/{ => rejected}/2021-05-23-groups2.md (100%) rename rfcs/{ => rejected}/2021-05-23-introduction.md (100%) diff --git a/migrations/20210101_initial.sql b/migrations/20210101_initial.sql index 75716865a..4c1e617c9 100644 --- a/migrations/20210101_initial.sql +++ b/migrations/20210101_initial.sql @@ -50,7 +50,7 @@ CREATE TABLE IF NOT EXISTS connections( snd_host TEXT, snd_port TEXT, snd_id BLOB, - last_internal_msg_id INTEGER NOT NULL, + last_internal_msg_id INTEGER NOT NULL, -- TODO add defauls here and below in the new schema last_internal_rcv_msg_id INTEGER NOT NULL, last_internal_snd_msg_id INTEGER NOT NULL, last_external_snd_msg_id INTEGER NOT NULL, diff --git a/migrations/20211202_connection_mode.sql b/migrations/20211202_connection_mode.sql new file mode 100644 index 000000000..d7e4e5ac6 --- /dev/null +++ b/migrations/20211202_connection_mode.sql @@ -0,0 +1,11 @@ +ALTER TABLE connections ADD conn_mode TEXT NOT NULL DEFAULT 'INV'; + +CREATE TABLE conn_invitations ( + invitation_id BLOB NOT NULL PRIMARY KEY, + contact_conn_id BLOB NOT NULL REFERENCES connections ON DELETE CASCADE, + cr_invitation BLOB NOT NULL, + recipient_conn_info BLOB NOT NULL, + accepted INTEGER NOT NULL DEFAULT 0, + own_conn_info BLOB, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +) WITHOUT ROWID; diff --git a/protocol/agent-protocol.md b/protocol/agent-protocol.md index 00dbc873b..033763047 100644 --- a/protocol/agent-protocol.md +++ b/protocol/agent-protocol.md @@ -12,6 +12,10 @@ - [HELLO message](#hello-message) - [REPLY message](#reply-message) - [MSG message](#msg-message) + - [INV message](#inv-message) + - [ACK message](#ack-message) + - [NEW message](#new-message) + - [DEL message](#del-message) - [SMP agent commands](#smp-agent-commands) - [Client commands and server responses](#client-commands-and-server-responses) - [NEW command and INV response](#new-command-and-inv-response) @@ -133,7 +137,7 @@ previousMsgHash = encoded encoded = agentMessage = helloMsg / replyQueueMsg / - clientMsg / acknowledgeMsg / + clientMsg / invitationMsg/ acknowledgeMsg / newQueueMessage / deleteQueueMsg msgPadding = *OCTET ; optional random bytes to get messages to the same size (as defined in SMP message size) @@ -149,6 +153,9 @@ clientMsg = %s"MSG" SP size CRLF clientMsgBody CRLF ; CRLF is in addition to CRL size = 1*DIGIT clientMsgBody = *OCTET +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 @@ -192,6 +199,10 @@ This is the message that is sent by the agent that received an out-of-band conne This is the agent envelope used to send client messages once the connection is established. Do not confuse it with the MSG response from SMP server to the agent and MSG response from SMP agent to the client that are sent in different contexts. +#### INV message + +This message is sent to the SMP queue(s) in `connReqContact`, to establish a new connection via existing unsecured queue, that acts as a permanent connection link of a user. + #### ACK message This message is sent to confirm the client message reception. It includes received message number, message hash and the reception status. @@ -225,17 +236,19 @@ agentCommand = (userCmd / agentMsg) CRLF userCmd = newCmd / joinCmd / acceptCmd / subscribeCmd / sendCmd / acknowledgeCmd / suspendCmd / deleteCmd agentMsg = invitation / connRequest / connInfo / connected / unsubscribed / connDown / connUp / messageId / sent / messageError / message / received / ok / error -newCmd = %s"NEW" [SP %s"NO_ACK"] ; response is `invitation` or `error` +newCmd = %s"NEW" SP connectionMode [SP %s"NO_ACK"] ; response is `invitation` or `error` ; NO_ACK parameter currently not supported -invitation = %s"INV" SP ; `connectionRequest` is defined below +connectionMode = %s"INV" / %s"CON" -connRequest = %s"REQ" SP confirmationId SP msgBody +invitation = %s"INV" SP connectionRequest ; `connectionRequest` is defined below + +connRequest = %s"REQ" SP connectionMode SP confirmationId SP msgBody ; msgBody here is any binary information identifying connection request confirmationId = 1*DIGIT -acceptCmd = %s"ACPT" SP confirmationId SP msgBody +acceptCmd = %s"ACPT" SP connectionMode SP confirmationId SP msgBody ; msgBody here is any binary information identifying connecting party connInfo = %s"INFO" SP msgBody @@ -255,9 +268,10 @@ connDown = %s"DOWN" connUp = %s"UP" ; restored connection -joinCmd = %s"JOIN" SP [SP %s"NO_REPLY"] [SP %s"NO_ACK"] -; `connectionRequest` is defined below +joinCmd = %s"JOIN" SP connectionRequest SP connInfo [SP %s"NO_REPLY"] [SP %s"NO_ACK"] +; `connectionRequest` and `connInfo` are defined below ; response is `connected` or `error` +; parameters NO_REPLY and NO_ACK are currently not supported suspendCmd = %s"OFF" ; can be sent by either party, response `ok` or `error` @@ -318,6 +332,8 @@ error = %s"ERR" SP `INV` response is sent by the agent to the client of the initiating party. +`NEW` command has `connectionMode` parameter to define the connection mode - to be used to communicate with a single contact (invitation mode, `connectionMode` is `INV`) or to accept connection requests from anybody (contact mode, `connectionMode` is `CON`). The type of connection request is determined by `connectionMode` parameter. + #### JOIN command It is used to create a connection and accept the connection request received out-of-band. It should be used by the client of the agent that accepts the connection (the joining party). @@ -382,9 +398,11 @@ Connection request `connectionRequest` is generated by SMP agent in response to Connection request syntax: ``` -connectionRequest = connectionProtocol "/" action "#/?smp=" smpQueues "&e2e=" e2eEncryption -action = %s"connect" -connectionProtocol = (%s"https://" clientAppServer) | %s"simplex:" +connectionRequest = connectionScheme "/" connReqType "#/?smp=" smpQueues "&e2e=" e2eEncryption +connReqType = %s"invitation" / %s"contact" +; this parameter has the same meaning as connectionMode in agent commands +; `NEW INV` creates `invitation` connection request, `NEW CON` - `contact` +connectionScheme = (%s"https://" clientAppServer) | %s"simplex:" clientAppServer = hostname [ ":" port ] ; client app server, e.g. simplex.chat e2eEncryption = encryptionScheme ":" publicKey diff --git a/rfcs/2021-05-17-open-connection.md b/rfcs/2021-05-17-open-connection.md new file mode 100644 index 000000000..fc83f137b --- /dev/null +++ b/rfcs/2021-05-17-open-connection.md @@ -0,0 +1,52 @@ +# Open connections + +## Problem + +This proposal describes how to create invitations that can be used multiple times. + +It can be used for: +- an open invitation to join a group. +- an open invitation to connect to a person - e.g. QR code/invitation link on a person's website, or passed from one person to another. +- part of the solution for public DNS-based addresses (when a directory server would map address in some domain name#example.tld to an open invitation). + +## Solution + +No changes to SMP protocol - a dedicated unsecured SMP queue is used to receive invitations to connect that are sent in encrypted agent message. An unsecured SMP queue is used as an out-of-band channel for establishing another SMP queue. + +Additional parameters in commands in SMP agent protocol: + +- `NEW` command will have a parameter `INV` or `CON` to create an invitation or a permanent contact connection. + +`conn_alias? OPEN` (or `PUB`, `NEWPUB`, tbc) - to create an "open"/"public" queue, the response is an invitation in a different format (TBC): + - should allow multiple servers (probably the original invitation should be extended to support it) + - should have a marker to indicate it's an open/public queue (probably the original invitation should be extended to include an invitation type). + +e.g. `smp:::/,/::` + +`queue_type`: + - `prv` - original invitation, should be accepted with KEY SMP message + - `pub` - open invitation, should be accepted with INV SMP message (to be added to SMP protocol) + +```mmd +A ->> AA: oidA? OPEN +AA ->> A: oidA INV pub_inv + +... + +B ->> BA: cidBA? JOIN pub_inv len CRLF meta_binary CRLF ; change command to require meta, len can be 0 for the current usage ; meta is used to send user profile +BA ->> B: cidBA OK +BA ->> SA ->> AA: INV prv_inv CRLF meta_binary +AA ->> A: oidA CONF invID len meta_binary +A ->> AA cidAB? LET invID + +establish connection as usual + +BA ->> B: cidBA CON +AA ->> A: cidAB CON +``` + +That protocol requires addressing the current problem when an invitation cannot be accepted when the party that generated the invitation is not online. + +Questions. + +1. Do we need to differentiate the semantics of the invitation on the syntax level, or should we allow to just manage it outside of protocol when the receiving agent decides which SMP messages to accept and which to ignore (KEY / INV). \ No newline at end of file diff --git a/rfcs/2021-01-20-logging.md b/rfcs/done/2021-01-20-logging.md similarity index 100% rename from rfcs/2021-01-20-logging.md rename to rfcs/done/2021-01-20-logging.md diff --git a/rfcs/2021-01-26-crypto.md b/rfcs/done/2021-01-26-crypto.md similarity index 100% rename from rfcs/2021-01-26-crypto.md rename to rfcs/done/2021-01-26-crypto.md diff --git a/rfcs/2021-01-26-messages.md b/rfcs/done/2021-01-26-messages.md similarity index 100% rename from rfcs/2021-01-26-messages.md rename to rfcs/done/2021-01-26-messages.md diff --git a/rfcs/2021-05-23-broadcast.md b/rfcs/rejected/2021-05-23-broadcast.md similarity index 100% rename from rfcs/2021-05-23-broadcast.md rename to rfcs/rejected/2021-05-23-broadcast.md diff --git a/rfcs/2021-05-23-groups2.md b/rfcs/rejected/2021-05-23-groups2.md similarity index 100% rename from rfcs/2021-05-23-groups2.md rename to rfcs/rejected/2021-05-23-groups2.md diff --git a/rfcs/2021-05-23-introduction.md b/rfcs/rejected/2021-05-23-introduction.md similarity index 100% rename from rfcs/2021-05-23-introduction.md rename to rfcs/rejected/2021-05-23-introduction.md diff --git a/simplexmq.cabal b/simplexmq.cabal index 4bd0b59f0..53291b95b 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -4,7 +4,7 @@ cabal-version: 1.12 -- -- see: https://github.com/sol/hpack -- --- hash: eac6184d7efe4fc07606d5f6bd75f1821af768a087526ff08749385844580cec +-- hash: 1e44584019db4d35d25a97c553870b0960fe7a18b5296f0e49b8084c343276ab name: simplexmq version: 0.4.1 @@ -31,6 +31,7 @@ extra-source-files: migrations/20210101_initial.sql migrations/20210624_confirmations.sql migrations/20210809_snd_messages.sql + migrations/20211202_connection_mode.sql migrations/README.md library diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 3c14cbbff..81b16f2ac 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -4,6 +4,7 @@ {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} +{-# LANGUAGE KindSignatures #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NumericUnderscores #-} @@ -45,6 +46,7 @@ module Simplex.Messaging.Agent createConnection, joinConnection, acceptConnection, + acceptContact, subscribeConnection, sendMessage, ackMessage, @@ -129,17 +131,21 @@ 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 -> m (ConnId, ConnectionRequest) -createConnection c = withAgentEnv c $ newConn c "" +createConnection :: AgentErrorMonad m => AgentClient -> SConnectionMode c -> m (ConnId, ConnectionRequest c) +createConnection c cMode = withAgentEnv c $ newConn c "" cMode -- | Join SMP agent connection (JOIN command) -joinConnection :: AgentErrorMonad m => AgentClient -> ConnectionRequest -> ConnInfo -> m ConnId +joinConnection :: AgentErrorMonad m => AgentClient -> ConnectionRequest c -> ConnInfo -> m ConnId joinConnection c = withAgentEnv c .: joinConn c "" --- | Approve confirmation (LET command) +-- | Approve confirmation (ACPT INV command) acceptConnection :: AgentErrorMonad m => AgentClient -> ConnId -> ConfirmationId -> ConnInfo -> m () acceptConnection c = withAgentEnv c .:. acceptConnection' c +-- | Approve contact (ACPT CON command) +acceptContact :: AgentErrorMonad m => AgentClient -> ConfirmationId -> ConnInfo -> m ConnId +acceptContact c = withAgentEnv c .: acceptContact' c "" + -- | Subscribe to receive connection messages (SUB command) subscribeConnection :: AgentErrorMonad m => AgentClient -> ConnId -> m () subscribeConnection c = withAgentEnv c . subscribeConnection' c @@ -236,27 +242,32 @@ 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 -> second INV <$> newConn c connId - JOIN smpQueueUri connInfo -> (,OK) <$> joinConn c connId smpQueueUri connInfo - ACPT confId ownConnInfo -> acceptConnection' c connId confId ownConnInfo $> (connId, OK) + NEW (ACM cMode) -> second (INV . ACR cMode) <$> newConn c connId cMode + JOIN (ACR _ cReq) connInfo -> (,OK) <$> joinConn c connId cReq connInfo + ACPT (ACM cMode) confInvId ownConnInfo -> case cMode of + SCMInvitation -> acceptConnection' c connId confInvId ownConnInfo $> (connId, OK) + SCMContact -> (,OK) <$> acceptContact' c connId confInvId ownConnInfo SUB -> subscribeConnection' c connId $> (connId, OK) SEND msgBody -> (connId,) . MID <$> sendMessage' c connId msgBody ACK msgId -> ackMessage' c connId msgId $> (connId, OK) OFF -> suspendConnection' c connId $> (connId, OK) DEL -> deleteConnection' c connId $> (connId, OK) -newConn :: AgentMonad m => AgentClient -> ConnId -> m (ConnId, ConnectionRequest) -newConn c connId = do +newConn :: AgentMonad m => AgentClient -> ConnId -> SConnectionMode c -> m (ConnId, ConnectionRequest c) +newConn c connId cMode = do srv <- getSMPServer (rq, qUri, encryptKey) <- newRcvQueue c srv g <- asks idsDrg let cData = ConnData {connId} - connId' <- withStore $ \st -> createRcvConn st g cData rq + connId' <- withStore $ \st -> createRcvConn st g cData rq cMode addSubscription c rq connId' - pure (connId', ConnectionRequest simplexChat CRAConnect [qUri] encryptKey) + let crData = ConnReqData simplexChat [qUri] encryptKey + pure . (connId',) $ case cMode of + SCMInvitation -> CRInvitation crData + SCMContact -> CRContact crData -joinConn :: AgentMonad m => AgentClient -> ConnId -> ConnectionRequest -> ConnInfo -> m ConnId -joinConn c connId (ConnectionRequest _ CRAConnect (qUri :| _) encryptKey) cInfo = do +joinConn :: AgentMonad m => AgentClient -> ConnId -> ConnectionRequest c -> ConnInfo -> m ConnId +joinConn c connId (CRInvitation (ConnReqData _ (qUri :| _) encryptKey)) cInfo = do (sq, senderKey, verifyKey) <- newSndQueue qUri encryptKey g <- asks idsDrg cfg <- asks config @@ -265,6 +276,10 @@ joinConn c connId (ConnectionRequest _ CRAConnect (qUri :| _) encryptKey) cInfo confirmQueue c sq senderKey cInfo activateQueueJoining c connId' sq verifyKey $ retryInterval cfg pure connId' +joinConn c connId (CRContact (ConnReqData _ (qUri :| _) encryptKey)) cInfo = do + (connId', cReq) <- newConn c connId SCMInvitation + sendInvitation c qUri encryptKey cReq cInfo + pure connId' activateQueueJoining :: forall m. AgentMonad m => AgentClient -> ConnId -> SndQueue -> VerificationKey -> RetryInterval -> m () activateQueueJoining c connId sq verifyKey retryInterval = @@ -276,17 +291,27 @@ activateQueueJoining c connId sq verifyKey retryInterval = (rq, qUri', encryptKey) <- newRcvQueue c srv addSubscription c rq connId withStore $ \st -> upgradeSndConnToDuplex st connId rq - sendControlMessage c sq . REPLY $ ConnectionRequest CRSSimplex CRAConnect [qUri'] encryptKey + sendControlMessage c sq . REPLY $ CRInvitation $ ConnReqData CRSSimplex [qUri'] encryptKey --- | Approve confirmation (LET command) in Reader monad +-- | Approve confirmation (ACPT INV command) in Reader monad acceptConnection' :: AgentMonad m => AgentClient -> ConnId -> ConfirmationId -> ConnInfo -> m () -acceptConnection' c connId confId ownConnInfo = +acceptConnection' c connId confId ownConnInfo = do withStore (`getConn` connId) >>= \case - SomeConn SCRcv (RcvConnection _ rq) -> do + SomeConn _ (RcvConnection _ rq) -> do AcceptedConfirmation {senderKey} <- withStore $ \st -> acceptConfirmation st confId ownConnInfo processConfirmation c rq senderKey _ -> throwError $ CMD PROHIBITED +-- | Accept contact (ACPT CON command) in Reader monad +acceptContact' :: AgentMonad m => AgentClient -> ConnId -> InvitationId -> ConnInfo -> m ConnId +acceptContact' c connId invId ownConnInfo = do + Invitation {contactConnId, connReq} <- withStore (`getInvitation` invId) + withStore (`getConn` contactConnId) >>= \case + SomeConn _ ContactConnection {} -> do + withStore $ \st -> acceptInvitation st invId ownConnInfo + joinConn c connId connReq ownConnInfo + _ -> throwError $ CMD PROHIBITED + processConfirmation :: AgentMonad m => AgentClient -> RcvQueue -> SenderPublicKey -> m () processConfirmation c rq sndKey = do withStore $ \st -> setRcvQueueStatus st rq Confirmed @@ -316,6 +341,7 @@ subscribeConnection' c connId = Active -> throwError $ CONN SIMPLEX _ -> throwError $ INTERNAL "unexpected queue status" SomeConn _ (RcvConnection _ rq) -> subscribeQueue c rq connId + SomeConn _ (ContactConnection _ _rq) -> pure () where resumeDelivery :: SndQueue -> m () resumeDelivery SndQueue {server} = do @@ -499,6 +525,7 @@ processSMPTransmission c@AgentClient {subQ} (srv, rId, cmd) = do withStore (\st -> getRcvConn st srv rId) >>= \case SomeConn SCDuplex (DuplexConnection cData rq _) -> processSMP SCDuplex cData rq SomeConn SCRcv (RcvConnection cData rq) -> processSMP SCRcv cData rq + SomeConn SCContact (ContactConnection cData rq) -> processSMP SCContact cData rq _ -> atomically $ writeTBQueue subQ ("", "", ERR $ CONN NOT_FOUND) where processSMP :: SConnType c -> ConnData -> RcvQueue -> m () @@ -509,13 +536,14 @@ processSMPTransmission c@AgentClient {subQ} (srv, rId, cmd) = do msg <- decryptAndVerify rq msgBody let msgHash = C.sha256Hash msg case parseSMPMessage msg of - Left e -> notify $ ERR e + Left e -> notify (ERR e) >> sendAck c rq Right (SMPConfirmation senderKey cInfo) -> smpConfirmation senderKey cInfo >> sendAck c rq Right SMPMessage {agentMessage, senderMsgId, senderTimestamp, previousMsgHash} -> case agentMessage of HELLO verifyKey _ -> helloMsg verifyKey msgBody >> sendAck c rq REPLY cReq -> replyMsg cReq >> sendAck c rq A_MSG body -> agentClientMsg previousMsgHash (senderMsgId, senderTimestamp) (srvMsgId, srvTs) body msgHash + A_INV cReq cInfo -> smpInvitation cReq cInfo >> sendAck c rq SMP.END -> do removeSubscription c connId logServer "<--" c srv rId "END" @@ -539,7 +567,7 @@ processSMPTransmission c@AgentClient {subQ} (srv, rId, cmd) = do g <- asks idsDrg let newConfirmation = NewConfirmation {connId, senderKey, senderConnInfo = cInfo} confId <- withStore $ \st -> createConfirmation st g newConfirmation - notify $ REQ confId cInfo + notify $ REQ cmInvitation confId cInfo SCDuplex -> do notify $ INFO cInfo processConfirmation c rq senderKey @@ -558,8 +586,8 @@ processSMPTransmission c@AgentClient {subQ} (srv, rId, cmd) = do SCDuplex -> notifyConnected c connId _ -> pure () - replyMsg :: ConnectionRequest -> m () - replyMsg (ConnectionRequest _ CRAConnect (qUri :| _) encryptKey) = do + replyMsg :: ConnectionRequest 'CMInvitation -> m () + replyMsg (CRInvitation (ConnReqData _ (qUri :| _) encryptKey)) = do logServer "<--" c srv rId "MSG " case cType of SCRcv -> do @@ -584,6 +612,17 @@ processSMPTransmission c@AgentClient {subQ} (srv, rId, cmd) = do withStore $ \st -> createRcvMsg st connId rcvMsg notify $ MSG msgMeta msgBody + smpInvitation :: ConnectionRequest 'CMInvitation -> ConnInfo -> m () + smpInvitation connReq cInfo = do + logServer "<--" c srv rId "MSG " + case cType of + SCContact -> do + g <- asks idsDrg + let newInv = NewInvitation {contactConnId = connId, connReq, recipientConnInfo = cInfo} + invId <- withStore $ \st -> createInvitation st g newInv + notify $ REQ cmContact invId cInfo + _ -> prohibited + checkMsgIntegrity :: PrevExternalSndId -> ExternalSndId -> PrevRcvMsgHash -> ByteString -> MsgIntegrity checkMsgIntegrity prevExtSndId extSndId internalPrevMsgHash receivedPrevMsgHash | extSndId == prevExtSndId + 1 && internalPrevMsgHash == receivedPrevMsgHash = MsgOk diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index bc057b31f..e99486c7f 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -17,6 +17,7 @@ module Simplex.Messaging.Agent.Client subscribeQueue, addSubscription, sendConfirmation, + sendInvitation, RetryInterval (..), sendHello, secureQueue, @@ -322,6 +323,23 @@ sendHello c sq@SndQueue {server, sndId, sndPrivateKey} verifyKey ri = agentMessage = HELLO verifyKey ackMode } +sendInvitation :: forall m. AgentMonad m => AgentClient -> SMPQueueUri -> EncryptionKey -> ConnectionRequest 'CMInvitation -> ConnInfo -> m () +sendInvitation c SMPQueueUri {smpServer, senderId} encryptKey cReq connInfo = do + withLogSMP_ c smpServer senderId "SEND " $ \smp -> do + msg <- mkInvitation smp + liftSMP $ sendSMPMessage smp Nothing senderId msg + where + mkInvitation :: SMPClient -> m ByteString + mkInvitation smp = do + senderTimestamp <- liftIO getCurrentTime + encryptUnsigned smp encryptKey . serializeSMPMessage $ + SMPMessage + { senderMsgId = 0, + senderTimestamp, + previousMsgHash = "", + agentMessage = A_INV cReq connInfo + } + secureQueue :: AgentMonad m => AgentClient -> RcvQueue -> SenderPublicKey -> m () secureQueue c RcvQueue {server, rcvId, rcvPrivateKey} senderKey = withLogSMP c server rcvId "KEY " $ \smp -> @@ -361,6 +379,15 @@ decryptAndVerify RcvQueue {decryptKey, verifyKey} msg = verifyMessage verifyKey msg >>= liftError cryptoError . C.decrypt decryptKey +encryptUnsigned :: AgentMonad m => SMPClient -> EncryptionKey -> ByteString -> m ByteString +encryptUnsigned smp encryptKey msg = do + paddedSize <- asks $ (blockSize smp -) . reservedMsgSize + size <- asks $ rsaKeySize . config + liftError cryptoError $ do + enc <- C.encrypt encryptKey paddedSize msg + let sig = B.replicate size ' ' + pure $ sig <> enc + verifyMessage :: AgentMonad m => Maybe VerificationKey -> ByteString -> m ByteString verifyMessage verifyKey msg = do size <- asks $ rsaKeySize . config diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index 8ae384ca5..706b638af 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -10,6 +10,7 @@ {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-} {-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-} @@ -38,10 +39,17 @@ module Simplex.Messaging.Agent.Protocol AMessage (..), SMPServer (..), SMPQueueUri (..), + ConnectionMode (..), + SConnectionMode (..), + AConnectionMode (..), + cmInvitation, + cmContact, + ConnectionModeI (..), ConnectionRequest (..), + AConnectionRequest (..), + ConnReqData (..), ConnReqScheme (..), simplexChat, - ConnReqAction (..), AgentErrorType (..), CommandErrorType (..), ConnectionErrorType (..), @@ -52,7 +60,6 @@ module Simplex.Messaging.Agent.Protocol ARawTransmission, ConnId, ConfirmationId, - IntroId, InvitationId, AckMode (..), OnOff (..), @@ -73,13 +80,20 @@ module Simplex.Messaging.Agent.Protocol serializeServer, serializeSMPQueueUri, reservedServerKey, -- TODO remove + serializeConnMode, + serializeConnMode', + connMode, + connMode', serializeConnReq, + serializeConnReq', serializeAgentError, commandP, parseSMPMessage, smpServerP, smpQueueUriP, + connModeT, connReqP, + connReqP', msgIntegrityP, agentErrorTypeP, agentMessageP, @@ -106,7 +120,9 @@ import Data.Int (Int64) import Data.Kind (Type) import Data.List (find) import qualified Data.List.NonEmpty as L +import Data.Maybe (isJust) import Data.String (IsString (..)) +import Data.Text (Text) import Data.Time.Clock (UTCTime) import Data.Time.ISO8601 import Data.Type.Equality @@ -128,7 +144,7 @@ import Simplex.Messaging.Transport (Transport (..), TransportError, serializeTra import Simplex.Messaging.Util import Test.QuickCheck (Arbitrary (..)) import Text.Read -import UnliftIO.Exception +import UnliftIO.Exception (Exception) -- | Raw (unparsed) SMP agent protocol transmission. type ARawTransmission = (ByteString, ByteString, ByteString) @@ -167,11 +183,11 @@ type ConnInfo = ByteString -- | Parameterized type for SMP agent protocol commands and responses from all participants. data ACommand (p :: AParty) where - NEW :: ACommand Client -- response INV - INV :: ConnectionRequest -> ACommand Agent - JOIN :: ConnectionRequest -> ConnInfo -> ACommand Client -- response OK - REQ :: ConfirmationId -> ConnInfo -> ACommand Agent -- ConnInfo is from sender - ACPT :: ConfirmationId -> ConnInfo -> ACommand Client -- ConnInfo is from client + NEW :: AConnectionMode -> ACommand Client -- response INV + INV :: AConnectionRequest -> ACommand Agent + JOIN :: AConnectionRequest -> ConnInfo -> ACommand Client -- response OK + REQ :: AConnectionMode -> ConfOrInvId -> ConnInfo -> ACommand Agent -- ConnInfo is from sender + ACPT :: AConnectionMode -> ConfOrInvId -> ConnInfo -> ACommand Client -- ConnInfo is from client INFO :: ConnInfo -> ACommand Agent CON :: ACommand Agent -- notification that connection is established SUB :: ACommand Client @@ -196,6 +212,49 @@ deriving instance Eq (ACommand p) deriving instance Show (ACommand p) +data ConnectionMode = CMInvitation | CMContact + deriving (Eq, Show) + +data SConnectionMode (m :: ConnectionMode) where + SCMInvitation :: SConnectionMode CMInvitation + SCMContact :: SConnectionMode CMContact + +deriving instance Eq (SConnectionMode m) + +deriving instance Show (SConnectionMode m) + +instance TestEquality SConnectionMode where + testEquality SCMInvitation SCMInvitation = Just Refl + testEquality SCMContact SCMContact = Just Refl + testEquality _ _ = Nothing + +data AConnectionMode = forall m. ACM (SConnectionMode m) + +instance Eq AConnectionMode where + ACM m == ACM m' = isJust $ testEquality m m' + +cmInvitation :: AConnectionMode +cmInvitation = ACM SCMInvitation + +cmContact :: AConnectionMode +cmContact = ACM SCMContact + +deriving instance Show AConnectionMode + +connMode :: SConnectionMode m -> ConnectionMode +connMode SCMInvitation = CMInvitation +connMode SCMContact = CMContact + +connMode' :: ConnectionMode -> AConnectionMode +connMode' CMInvitation = cmInvitation +connMode' CMContact = cmContact + +class ConnectionModeI (m :: ConnectionMode) where sConnectionMode :: SConnectionMode m + +instance ConnectionModeI CMInvitation where sConnectionMode = SCMInvitation + +instance ConnectionModeI CMContact where sConnectionMode = SCMContact + type MsgHash = ByteString -- | Agent message metadata sent to the client @@ -238,9 +297,11 @@ data AMessage where -- | the first message in the queue to validate it is secured HELLO :: VerificationKey -> AckMode -> AMessage -- | reply queue information - REPLY :: ConnectionRequest -> AMessage + REPLY :: ConnectionRequest CMInvitation -> AMessage -- | agent envelope for the client message A_MSG :: MsgBody -> AMessage + -- | connection request with the invitation to connect + A_INV :: ConnectionRequest CMInvitation -> ConnInfo -> AMessage deriving (Show) -- | Parse SMP message. @@ -281,10 +342,12 @@ agentMessageP = "HELLO " *> hello <|> "REPLY " *> reply <|> "MSG " *> a_msg + <|> "INV " *> a_inv where hello = HELLO <$> C.pubKeyP <*> ackMode - reply = REPLY <$> connReqP + reply = REPLY <$> connReqP' a_msg = A_MSG <$> binaryBodyP <* A.endOfLine + a_inv = A_INV <$> connReqP' <* A.space <*> binaryBodyP <* A.endOfLine ackMode = AckMode <$> (" NO_ACK" $> Off <|> pure On) -- | SMP server location parser. @@ -298,8 +361,9 @@ smpServerP = SMPServer <$> server <*> optional port <*> optional kHash serializeAgentMessage :: AMessage -> ByteString serializeAgentMessage = \case HELLO verifyKey ackMode -> "HELLO " <> C.serializePubKey verifyKey <> if ackMode == AckMode Off then " NO_ACK" else "" - REPLY cReq -> "REPLY " <> serializeConnReq cReq + REPLY cReq -> "REPLY " <> serializeConnReq' cReq A_MSG body -> "MSG " <> serializeBinary body <> "\n" + A_INV cReq cInfo -> B.unwords ["INV", serializeConnReq' cReq, serializeBinary cInfo] <> "\n" -- | Serialize SMP queue information that is sent out-of-band. serializeSMPQueueUri :: SMPQueueUri -> ByteString @@ -314,31 +378,50 @@ smpQueueUriP = reservedServerKey :: C.PublicKey reservedServerKey = C.PublicKey $ R.PublicKey 1 0 0 -serializeConnReq :: ConnectionRequest -> ByteString -serializeConnReq (ConnectionRequest scheme action smpQueues encryptionKey) = - sch <> "/" <> act <> "#/" <> queryStr - where - sch = case scheme of - CRSSimplex -> "simplex:" - CRSAppServer host port -> B.pack $ "https://" <> host <> maybe "" (':' :) port - act = case action of - CRAConnect -> "connect" - queryStr = renderSimpleQuery True [("smp", queues), ("e2e", key)] - queues = B.intercalate "," . map serializeSMPQueueUri $ L.toList smpQueues - key = C.serializePubKey encryptionKey +serializeConnReq :: AConnectionRequest -> ByteString +serializeConnReq (ACR _ cr) = serializeConnReq' cr -connReqP :: Parser ConnectionRequest +serializeConnReq' :: ConnectionRequest m -> ByteString +serializeConnReq' = \case + CRInvitation crData -> serialize CMInvitation crData + CRContact crData -> serialize CMContact crData + where + serialize crMode ConnReqData {crScheme, crSmpQueues, crEncryptKey} = + sch <> "/" <> m <> "#/" <> queryStr + where + sch = case crScheme of + CRSSimplex -> "simplex:" + CRSAppServer host port -> B.pack $ "https://" <> host <> maybe "" (':' :) port + m = case crMode of + CMInvitation -> "invitation" + CMContact -> "contact" + queryStr = renderSimpleQuery True [("smp", queues), ("e2e", key)] + queues = B.intercalate "," . map serializeSMPQueueUri $ L.toList crSmpQueues + key = C.serializePubKey crEncryptKey + +connReqP' :: forall m. ConnectionModeI m => Parser (ConnectionRequest m) +connReqP' = do + ACR m cr <- connReqP + case testEquality m $ sConnectionMode @m of + Just Refl -> pure cr + _ -> fail "bad connection request mode" + +connReqP :: Parser AConnectionRequest connReqP = do crScheme <- "simplex:" $> CRSSimplex <|> "https://" *> appServer - crAction <- "/" *> ("connect" $> CRAConnect) <* "#/?" + crMode <- "/" *> mode <* "#/?" query <- parseSimpleQuery <$> A.takeTill (\c -> c == ' ' || c == '\n') crSmpQueues <- paramP "smp" smpQueues query crEncryptKey <- paramP "e2e" C.pubKeyP query - pure ConnectionRequest {crScheme, crAction, crSmpQueues, crEncryptKey} + let cReq = ConnReqData {crScheme, crSmpQueues, crEncryptKey} + pure $ case crMode of + CMInvitation -> ACR SCMInvitation $ CRInvitation cReq + CMContact -> ACR SCMContact $ CRContact cReq where appServer = CRSAppServer <$> host <*> optional port host = B.unpack <$> A.takeTill (\c -> c == ':' || c == '/') port = B.unpack <$> (A.char ':' *> A.takeTill (== '/')) + mode = "invitation" $> CMInvitation <|> "contact" $> CMContact paramP param parser query = let p = maybe (fail "") (pure . snd) $ find ((== param) . fst) query in parseAll parser <$?> p @@ -366,6 +449,26 @@ smpServerUriP = do port <- optional $ B.unpack <$> (A.char ':' *> A.takeWhile1 A.isDigit) pure SMPServer {host, port, keyHash} +serializeConnMode :: AConnectionMode -> ByteString +serializeConnMode (ACM cMode) = serializeConnMode' $ connMode cMode + +serializeConnMode' :: ConnectionMode -> ByteString +serializeConnMode' = \case + CMInvitation -> "INV" + CMContact -> "CON" + +connModeP' :: Parser ConnectionMode +connModeP' = "INV" $> CMInvitation <|> "CON" $> CMContact + +connModeP :: Parser AConnectionMode +connModeP = connMode' <$> connModeP' + +connModeT :: Text -> Maybe ConnectionMode +connModeT = \case + "INV" -> Just CMInvitation + "CON" -> Just CMContact + _ -> Nothing + -- | SMP server location and transport key digest (hash). data SMPServer = SMPServer { host :: HostName, @@ -382,10 +485,10 @@ type ConnId = ByteString type ConfirmationId = ByteString -type IntroId = ByteString - type InvitationId = ByteString +type ConfOrInvId = ByteString + -- | Connection modes. data OnOff = On | Off deriving (Eq, Show, Read) @@ -402,9 +505,25 @@ data SMPQueueUri = SMPQueueUri } deriving (Eq, Show) -data ConnectionRequest = ConnectionRequest +data ConnectionRequest (m :: ConnectionMode) where + CRInvitation :: ConnReqData -> ConnectionRequest CMInvitation + CRContact :: ConnReqData -> ConnectionRequest CMContact + +deriving instance Eq (ConnectionRequest m) + +deriving instance Show (ConnectionRequest m) + +data AConnectionRequest = forall m. ACR (SConnectionMode m) (ConnectionRequest m) + +instance Eq AConnectionRequest where + ACR m cr == ACR m' cr' = case testEquality m m' of + Just Refl -> cr == cr' + _ -> False + +deriving instance Show AConnectionRequest + +data ConnReqData = ConnReqData { crScheme :: ConnReqScheme, - crAction :: ConnReqAction, crSmpQueues :: L.NonEmpty SMPQueueUri, crEncryptKey :: EncryptionKey } @@ -416,8 +535,6 @@ data ConnReqScheme = CRSSimplex | CRSAppServer HostName (Maybe ServiceName) simplexChat :: ConnReqScheme simplexChat = CRSAppServer "simplex.chat" Nothing -data ConnReqAction = CRAConnect deriving (Eq, Show) - -- | Public key used to E2E encrypt SMP messages. type EncryptionKey = C.PublicKey @@ -537,7 +654,7 @@ instance Arbitrary SMPAgentError where arbitrary = genericArbitraryU -- | SMP agent command and response parser commandP :: Parser ACmd commandP = - "NEW" $> ACmd SClient NEW + "NEW " *> newCmd <|> "INV " *> invResp <|> "JOIN " *> joinCmd <|> "REQ " *> reqCmd @@ -559,10 +676,11 @@ commandP = <|> "CON" $> ACmd SAgent CON <|> "OK" $> ACmd SAgent OK where + newCmd = ACmd SClient . NEW <$> connModeP invResp = ACmd SAgent . INV <$> connReqP joinCmd = ACmd SClient <$> (JOIN <$> connReqP <* A.space <*> A.takeByteString) - reqCmd = ACmd SAgent <$> (REQ <$> A.takeTill (== ' ') <* A.space <*> A.takeByteString) - acptCmd = ACmd SClient <$> (ACPT <$> A.takeTill (== ' ') <* A.space <*> A.takeByteString) + reqCmd = ACmd SAgent <$> (REQ <$> connModeP <* A.space <*> A.takeTill (== ' ') <* A.space <*> A.takeByteString) + acptCmd = ACmd SClient <$> (ACPT <$> connModeP <* A.space <*> A.takeTill (== ' ') <* A.space <*> A.takeByteString) infoCmd = ACmd SAgent . INFO <$> A.takeByteString sendCmd = ACmd SClient . SEND <$> A.takeByteString msgIdResp = ACmd SAgent . MID <$> A.decimal @@ -595,11 +713,11 @@ parseCommand = parse commandP $ CMD SYNTAX -- | Serialize SMP agent command. serializeCommand :: ACommand p -> ByteString serializeCommand = \case - NEW -> "NEW" + NEW cMode -> "NEW " <> serializeConnMode cMode INV cReq -> "INV " <> serializeConnReq cReq - JOIN cReq cInfo -> "JOIN " <> serializeConnReq cReq <> " " <> serializeBinary cInfo - REQ confId cInfo -> "REQ " <> confId <> " " <> serializeBinary cInfo - ACPT confId cInfo -> "ACPT " <> confId <> " " <> serializeBinary cInfo + JOIN cReq cInfo -> B.unwords ["JOIN", serializeConnReq cReq, serializeBinary cInfo] + REQ cMode confId cInfo -> B.unwords ["REQ", serializeConnMode cMode, confId, serializeBinary cInfo] + ACPT cMode confId cInfo -> B.unwords ["ACPT", serializeConnMode cMode, confId, serializeBinary cInfo] INFO cInfo -> "INFO " <> serializeBinary cInfo SUB -> "SUB" END -> "END" @@ -608,9 +726,8 @@ serializeCommand = \case SEND msgBody -> "SEND " <> serializeBinary msgBody MID mId -> "MID " <> bshow mId SENT mId -> "SENT " <> bshow mId - MERR mId e -> "MERR " <> bshow mId <> " " <> serializeAgentError e - MSG msgMeta msgBody -> - "MSG " <> serializeMsgMeta msgMeta <> " " <> serializeBinary msgBody + MERR mId e -> B.unwords ["MERR", bshow mId, serializeAgentError e] + MSG msgMeta msgBody -> B.unwords ["MSG", serializeMsgMeta msgMeta, serializeBinary msgBody] ACK mId -> "ACK " <> bshow mId OFF -> "OFF" DEL -> "DEL" @@ -700,8 +817,9 @@ tGet party h = liftIO (tGetRaw h) >>= tParseLoadBody tConnId :: ARawTransmission -> ACommand p -> Either AgentErrorType (ACommand p) tConnId (_, connId, _) cmd = case cmd of -- NEW, JOIN and ACPT have optional connId - NEW -> Right cmd + NEW _ -> Right cmd JOIN {} -> Right cmd + ACPT {} -> Right cmd -- ERROR response does not always have connId ERR _ -> Right cmd -- other responses must have connId @@ -714,8 +832,8 @@ tGet party h = liftIO (tGetRaw h) >>= tParseLoadBody SEND body -> SEND <$$> getBody body MSG msgMeta body -> MSG msgMeta <$$> getBody body JOIN qUri cInfo -> JOIN qUri <$$> getBody cInfo - REQ confId cInfo -> REQ confId <$$> getBody cInfo - ACPT confId cInfo -> ACPT confId <$$> getBody cInfo + REQ cMode confId cInfo -> REQ cMode confId <$$> getBody cInfo + ACPT cMode confId cInfo -> ACPT cMode confId <$$> getBody cInfo INFO cInfo -> INFO <$$> getBody cInfo cmd -> pure $ Right cmd diff --git a/src/Simplex/Messaging/Agent/Store.hs b/src/Simplex/Messaging/Agent/Store.hs index fd8b3ced6..b387779e6 100644 --- a/src/Simplex/Messaging/Agent/Store.hs +++ b/src/Simplex/Messaging/Agent/Store.hs @@ -32,7 +32,7 @@ import qualified Simplex.Messaging.Protocol as SMP -- | Store class type. Defines store access methods for implementations. class Monad m => MonadAgentStore s m where -- Queue and Connection management - createRcvConn :: s -> TVar ChaChaDRG -> ConnData -> RcvQueue -> m ConnId + createRcvConn :: s -> TVar ChaChaDRG -> ConnData -> RcvQueue -> SConnectionMode c -> m ConnId createSndConn :: s -> TVar ChaChaDRG -> ConnData -> SndQueue -> m ConnId getConn :: s -> ConnId -> m SomeConn getAllConnIds :: s -> m [ConnId] -- TODO remove - hack for subscribing to all @@ -51,6 +51,11 @@ class Monad m => MonadAgentStore s m where getAcceptedConfirmation :: s -> ConnId -> m AcceptedConfirmation removeConfirmations :: s -> ConnId -> m () + -- Invitations - sent via Contact connections + createInvitation :: s -> TVar ChaChaDRG -> NewInvitation -> m InvitationId + getInvitation :: s -> InvitationId -> m Invitation + acceptInvitation :: s -> InvitationId -> ConnInfo -> m () + -- Msg management updateRcvIds :: s -> ConnId -> m (InternalId, InternalRcvId, PrevExternalSndId, PrevRcvMsgHash) createRcvMsg :: s -> ConnId -> RcvMsgData -> m () @@ -91,7 +96,7 @@ data SndQueue = SndQueue -- * Connection types -- | Type of a connection. -data ConnType = CRcv | CSnd | CDuplex deriving (Eq, Show) +data ConnType = CRcv | CSnd | CDuplex | CContact deriving (Eq, Show) -- | Connection of a specific type. -- @@ -107,6 +112,7 @@ data Connection (d :: ConnType) where RcvConnection :: ConnData -> RcvQueue -> Connection CRcv SndConnection :: ConnData -> SndQueue -> Connection CSnd DuplexConnection :: ConnData -> RcvQueue -> SndQueue -> Connection CDuplex + ContactConnection :: ConnData -> RcvQueue -> Connection CContact deriving instance Eq (Connection d) @@ -116,11 +122,13 @@ data SConnType :: ConnType -> Type where SCRcv :: SConnType CRcv SCSnd :: SConnType CSnd SCDuplex :: SConnType CDuplex + SCContact :: SConnType CContact connType :: SConnType c -> ConnType connType SCRcv = CRcv connType SCSnd = CSnd connType SCDuplex = CDuplex +connType SCContact = CContact deriving instance Eq (SConnType d) @@ -130,6 +138,7 @@ instance TestEquality SConnType where testEquality SCRcv SCRcv = Just Refl testEquality SCSnd SCSnd = Just Refl testEquality SCDuplex SCDuplex = Just Refl + testEquality SCContact SCContact = Just Refl testEquality _ _ = Nothing -- | Connection of an unknown type. @@ -162,6 +171,23 @@ data AcceptedConfirmation = AcceptedConfirmation ownConnInfo :: ConnInfo } +-- * Invitations + +data NewInvitation = NewInvitation + { contactConnId :: ConnId, + connReq :: ConnectionRequest 'CMInvitation, + recipientConnInfo :: ConnInfo + } + +data Invitation = Invitation + { invitationId :: InvitationId, + contactConnId :: ConnId, + connReq :: ConnectionRequest 'CMInvitation, + recipientConnInfo :: ConnInfo, + ownConnInfo :: Maybe ConnInfo, + accepted :: Bool + } + -- * Message integrity validation types -- | Corresponds to `last_external_snd_msg_id` in `connections` table @@ -320,6 +346,8 @@ data StoreError SEBadConnType ConnType | -- | Confirmation not found. SEConfirmationNotFound + | -- | Invitation not found + SEInvitationNotFound | -- | Message not found SEMsgNotFound | -- | Currently not used. The intention was to pass current expected queue status in methods, diff --git a/src/Simplex/Messaging/Agent/Store/SQLite.hs b/src/Simplex/Messaging/Agent/Store/SQLite.hs index e76729069..13d1819d9 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite.hs @@ -39,6 +39,7 @@ import Data.List (find) import Data.Maybe (fromMaybe) import Data.Text (Text) import qualified Data.Text as T +import Data.Text.Encoding (decodeLatin1) import Database.SQLite.Simple (FromRow, NamedParam (..), Only (..), SQLData (..), SQLError, field) import qualified Database.SQLite.Simple as DB import Database.SQLite.Simple.FromField @@ -150,8 +151,8 @@ withTransaction st action = withConnection st $ loop 100 100_000 else E.throwIO e instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteStore m where - createRcvConn :: SQLiteStore -> TVar ChaChaDRG -> ConnData -> RcvQueue -> m ConnId - createRcvConn st gVar cData q@RcvQueue {server} = + createRcvConn :: SQLiteStore -> TVar ChaChaDRG -> ConnData -> RcvQueue -> SConnectionMode c -> m ConnId + createRcvConn st gVar cData q@RcvQueue {server} cMode = -- TODO if schema has to be restarted, this function can be refactored -- to create connection first using createWithRandomId liftIOEither . checkConstraint SEConnDuplicate . withTransaction st $ \db -> @@ -161,7 +162,7 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto create db connId = do upsertServer_ db server insertRcvQueue_ db connId q - insertRcvConnection_ db cData {connId} q + insertRcvConnection_ db cData {connId} q cMode pure connId createSndConn :: SQLiteStore -> TVar ChaChaDRG -> ConnData -> SndQueue -> m ConnId @@ -359,6 +360,50 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto |] [":conn_alias" := connId] + createInvitation :: SQLiteStore -> TVar ChaChaDRG -> NewInvitation -> m InvitationId + createInvitation st gVar NewInvitation {contactConnId, connReq, recipientConnInfo} = + liftIOEither . withTransaction st $ \db -> + createWithRandomId gVar $ \invitationId -> + DB.execute + db + [sql| + INSERT INTO conn_invitations + (invitation_id, contact_conn_id, cr_invitation, recipient_conn_info, accepted) VALUES (?, ?, ?, ?, 0); + |] + (invitationId, contactConnId, connReq, recipientConnInfo) + + getInvitation :: SQLiteStore -> InvitationId -> m Invitation + getInvitation st invitationId = + liftIOEither . withTransaction st $ \db -> + invitation + <$> DB.query + db + [sql| + SELECT contact_conn_id, cr_invitation, recipient_conn_info, own_conn_info, accepted + FROM conn_invitations + WHERE invitation_id = ? + |] + (Only invitationId) + where + invitation [(contactConnId, connReq, recipientConnInfo, ownConnInfo, accepted)] = + Right Invitation {invitationId, contactConnId, connReq, recipientConnInfo, ownConnInfo, accepted} + invitation _ = Left SEInvitationNotFound + + acceptInvitation :: SQLiteStore -> InvitationId -> ConnInfo -> m () + acceptInvitation st invitationId ownConnInfo = + liftIO . withTransaction st $ \db -> do + DB.executeNamed + db + [sql| + UPDATE conn_invitations + SET accepted = 1, + own_conn_info = :own_conn_info + WHERE invitation_id = :invitation_id + |] + [ ":own_conn_info" := ownConnInfo, + ":invitation_id" := invitationId + ] + updateRcvIds :: SQLiteStore -> ConnId -> m (InternalId, InternalRcvId, PrevExternalSndId, PrevRcvMsgHash) updateRcvIds st connId = liftIO . withTransaction st $ \db -> do @@ -505,9 +550,21 @@ instance ToField SMPQueueUri where toField = toField . serializeSMPQueueUri instance FromField SMPQueueUri where fromField = blobFieldParser smpQueueUriP -instance ToField ConnectionRequest where toField = toField . serializeConnReq +instance ToField AConnectionRequest where toField = toField . serializeConnReq -instance FromField ConnectionRequest where fromField = blobFieldParser connReqP +instance FromField AConnectionRequest where fromField = blobFieldParser connReqP + +instance ToField (ConnectionRequest c) where toField = toField . serializeConnReq' + +instance (E.Typeable c, ConnectionModeI c) => FromField (ConnectionRequest c) where fromField = blobFieldParser connReqP' + +instance ToField ConnectionMode where toField = toField . decodeLatin1 . serializeConnMode' + +instance FromField ConnectionMode where fromField = fromTextField_ connModeT + +instance ToField (SConnectionMode c) where toField = toField . connMode + +instance FromField AConnectionMode where fromField = fromTextField_ $ fmap connMode' . connModeT fromTextField_ :: (E.Typeable a) => (Text -> Maybe a) -> Field -> Ok a fromTextField_ fromText = \case @@ -568,21 +625,24 @@ insertRcvQueue_ dbConn connId RcvQueue {..} = do ":status" := status ] -insertRcvConnection_ :: DB.Connection -> ConnData -> RcvQueue -> IO () -insertRcvConnection_ dbConn ConnData {connId} RcvQueue {server, rcvId} = do +insertRcvConnection_ :: DB.Connection -> ConnData -> RcvQueue -> SConnectionMode c -> IO () +insertRcvConnection_ dbConn ConnData {connId} RcvQueue {server, rcvId} cMode = do let port_ = serializePort_ $ port server DB.executeNamed dbConn [sql| INSERT INTO connections - ( conn_alias, rcv_host, rcv_port, rcv_id, snd_host, snd_port, snd_id, last_internal_msg_id, last_internal_rcv_msg_id, last_internal_snd_msg_id, last_external_snd_msg_id, last_rcv_msg_hash, last_snd_msg_hash) + ( conn_alias, rcv_host, rcv_port, rcv_id, snd_host, snd_port, snd_id, last_internal_msg_id, last_internal_rcv_msg_id, last_internal_snd_msg_id, last_external_snd_msg_id, last_rcv_msg_hash, last_snd_msg_hash, + conn_mode ) VALUES - (:conn_alias,:rcv_host,:rcv_port,:rcv_id, NULL, NULL, NULL, 0, 0, 0, 0, x'', x''); + (:conn_alias,:rcv_host,:rcv_port,:rcv_id, NULL, NULL, NULL, 0, 0, 0, 0, x'', x'', + :conn_mode ); |] [ ":conn_alias" := connId, ":rcv_host" := host server, ":rcv_port" := port_, - ":rcv_id" := rcvId + ":rcv_id" := rcvId, + ":conn_mode" := cMode ] -- * createSndConn helpers @@ -631,21 +691,22 @@ getConn_ :: DB.Connection -> ConnId -> IO (Either StoreError SomeConn) getConn_ dbConn connId = getConnData_ dbConn connId >>= \case Nothing -> pure $ Left SEConnNotFound - Just connData -> do + Just (connData, cMode) -> do rQ <- getRcvQueueByConnAlias_ dbConn connId sQ <- getSndQueueByConnAlias_ dbConn connId - pure $ case (rQ, sQ) of - (Just rcvQ, Just sndQ) -> Right $ SomeConn SCDuplex (DuplexConnection connData rcvQ sndQ) - (Just rcvQ, Nothing) -> Right $ SomeConn SCRcv (RcvConnection connData rcvQ) - (Nothing, Just sndQ) -> Right $ SomeConn SCSnd (SndConnection connData sndQ) + pure $ case (rQ, sQ, cMode) of + (Just rcvQ, Just sndQ, CMInvitation) -> Right $ SomeConn SCDuplex (DuplexConnection connData rcvQ sndQ) + (Just rcvQ, Nothing, CMInvitation) -> Right $ SomeConn SCRcv (RcvConnection connData rcvQ) + (Nothing, Just sndQ, CMInvitation) -> Right $ SomeConn SCSnd (SndConnection connData sndQ) + (Just rcvQ, Nothing, CMContact) -> Right $ SomeConn SCContact (ContactConnection connData rcvQ) _ -> Left SEConnNotFound -getConnData_ :: DB.Connection -> ConnId -> IO (Maybe ConnData) +getConnData_ :: DB.Connection -> ConnId -> IO (Maybe (ConnData, ConnectionMode)) getConnData_ dbConn connId' = connData - <$> DB.query dbConn "SELECT conn_alias FROM connections WHERE conn_alias = ?;" (Only connId') + <$> DB.query dbConn "SELECT conn_alias, conn_mode FROM connections WHERE conn_alias = ?;" (Only connId') where - connData [Only connId] = Just ConnData {connId} + connData [(connId, cMode)] = Just (ConnData {connId}, cMode) connData _ = Nothing getRcvQueueByConnAlias_ :: DB.Connection -> ConnId -> IO (Maybe RcvQueue) diff --git a/tests/AgentTests.hs b/tests/AgentTests.hs index 0777b7898..fc115b6d4 100644 --- a/tests/AgentTests.hs +++ b/tests/AgentTests.hs @@ -11,7 +11,7 @@ module AgentTests (agentTests) where import AgentTests.ConnectionRequestTests -import AgentTests.FunctionalAPITests (functionalAPITests) +import AgentTests.FunctionalAPITests (functionalAPITests, pattern REQ_CON, pattern REQ_INV) import AgentTests.SQLiteTests (storeTests) import Control.Concurrent import Data.ByteString.Char8 (ByteString) @@ -45,6 +45,11 @@ agentTests (ATransport t) = do smpAgentTest2_2_2 $ testDuplexConnection t it "should connect via 2 servers and 2 agents (random IDs)" $ smpAgentTest2_2_2 $ testDuplexConnRandomIds t + describe "Establishing two connections via `contact connection" do + it "should connect via contact contact with one server and 3 agents" $ + smpAgentTest3 $ testContactConnection t + it "should connect via contact contact with one server and 2 agents (random IDs)" $ + smpAgentTest2_2_1 $ testContactConnRandomIds t describe "Connection subscriptions" do it "should connect via one server and one agent" $ smpAgentTest3_1_1 $ testSubscription t @@ -101,11 +106,11 @@ pattern Msg msgBody <- MSG MsgMeta {integrity = MsgOk} msgBody testDuplexConnection :: Transport c => TProxy c -> c -> c -> IO () testDuplexConnection _ alice bob = do - ("1", "bob", Right (INV cReq)) <- alice #: ("1", "bob", "NEW") + ("1", "bob", Right (INV cReq)) <- alice #: ("1", "bob", "NEW INV") let cReq' = serializeConnReq cReq bob #: ("11", "alice", "JOIN " <> cReq' <> " 14\nbob's connInfo") #> ("11", "alice", OK) - ("", "bob", Right (REQ confId "bob's connInfo")) <- (alice <#:) - alice #: ("2", "bob", "ACPT " <> confId <> " 16\nalice's connInfo") #> ("2", "bob", OK) + ("", "bob", Right (REQ_INV confId "bob's connInfo")) <- (alice <#:) + alice #: ("2", "bob", "ACPT INV " <> confId <> " 16\nalice's connInfo") #> ("2", "bob", OK) bob <# ("", "alice", INFO "alice's connInfo") bob <# ("", "alice", CON) alice <# ("", "bob", CON) @@ -133,12 +138,12 @@ testDuplexConnection _ alice bob = do testDuplexConnRandomIds :: Transport c => TProxy c -> c -> c -> IO () testDuplexConnRandomIds _ alice bob = do - ("1", bobConn, Right (INV cReq)) <- alice #: ("1", "", "NEW") + ("1", bobConn, Right (INV cReq)) <- alice #: ("1", "", "NEW INV") let cReq' = serializeConnReq cReq ("11", aliceConn, Right OK) <- bob #: ("11", "", "JOIN " <> cReq' <> " 14\nbob's connInfo") - ("", bobConn', Right (REQ confId "bob's connInfo")) <- (alice <#:) + ("", bobConn', Right (REQ_INV confId "bob's connInfo")) <- (alice <#:) bobConn' `shouldBe` bobConn - alice #: ("2", bobConn, "ACPT " <> confId <> " 16\nalice's connInfo") =#> \case ("2", c, OK) -> c == bobConn; _ -> False + alice #: ("2", bobConn, "ACPT INV " <> confId <> " 16\nalice's connInfo") =#> \case ("2", c, OK) -> c == bobConn; _ -> False bob <# ("", aliceConn, INFO "alice's connInfo") bob <# ("", aliceConn, CON) alice <# ("", bobConn, CON) @@ -164,6 +169,60 @@ testDuplexConnRandomIds _ alice bob = do alice #: ("6", bobConn, "DEL") #> ("6", bobConn, OK) alice #:# "nothing else should be delivered to alice" +testContactConnection :: Transport c => TProxy c -> c -> c -> c -> IO () +testContactConnection _ alice bob tom = do + ("1", "alice_contact", Right (INV cReq)) <- alice #: ("1", "alice_contact", "NEW CON") + let cReq' = serializeConnReq cReq + + bob #: ("11", "alice", "JOIN " <> cReq' <> " 14\nbob's connInfo") #> ("11", "alice", OK) + ("", "alice_contact", Right (REQ_CON aConfId "bob's connInfo")) <- (alice <#:) + alice #: ("2", "bob", "ACPT CON " <> aConfId <> " 16\nalice's connInfo") #> ("2", "bob", OK) + ("", "alice", Right (REQ_INV bConfId "alice's connInfo")) <- (bob <#:) + bob #: ("12", "alice", "ACPT INV " <> bConfId <> " 16\nbob's connInfo 2") #> ("12", "alice", OK) + alice <# ("", "bob", INFO "bob's connInfo 2") + alice <# ("", "bob", CON) + bob <# ("", "alice", CON) + alice #: ("3", "bob", "SEND :hi") #> ("3", "bob", MID 1) + alice <# ("", "bob", SENT 1) + bob <#= \case ("", "alice", Msg "hi") -> True; _ -> False + bob #: ("13", "alice", "ACK 1") #> ("13", "alice", OK) + + tom #: ("21", "alice", "JOIN " <> cReq' <> " 14\ntom's connInfo") #> ("21", "alice", OK) + ("", "alice_contact", Right (REQ_CON aConfId' "tom's connInfo")) <- (alice <#:) + alice #: ("4", "tom", "ACPT CON " <> aConfId' <> " 16\nalice's connInfo") #> ("4", "tom", OK) + ("", "alice", Right (REQ_INV tConfId "alice's connInfo")) <- (tom <#:) + tom #: ("22", "alice", "ACPT INV " <> tConfId <> " 16\ntom's connInfo 2") #> ("22", "alice", OK) + alice <# ("", "tom", INFO "tom's connInfo 2") + alice <# ("", "tom", CON) + tom <# ("", "alice", CON) + alice #: ("5", "tom", "SEND :hi there") #> ("5", "tom", MID 1) + alice <# ("", "tom", SENT 1) + tom <#= \case ("", "alice", Msg "hi there") -> True; _ -> False + tom #: ("23", "alice", "ACK 1") #> ("23", "alice", OK) + +testContactConnRandomIds :: Transport c => TProxy c -> c -> c -> IO () +testContactConnRandomIds _ alice bob = do + ("1", aliceContact, Right (INV cReq)) <- alice #: ("1", "", "NEW CON") + let cReq' = serializeConnReq cReq + + ("11", aliceConn, Right OK) <- bob #: ("11", "", "JOIN " <> cReq' <> " 14\nbob's connInfo") + ("", aliceContact', Right (REQ_CON aConfId "bob's connInfo")) <- (alice <#:) + aliceContact' `shouldBe` aliceContact + + ("2", bobConn, Right OK) <- alice #: ("2", "", "ACPT CON " <> aConfId <> " 16\nalice's connInfo") + ("", aliceConn', Right (REQ_INV bConfId "alice's connInfo")) <- (bob <#:) + aliceConn' `shouldBe` aliceConn + + bob #: ("12", aliceConn, "ACPT INV " <> bConfId <> " 16\nbob's connInfo 2") #> ("12", aliceConn, OK) + alice <# ("", bobConn, INFO "bob's connInfo 2") + alice <# ("", bobConn, CON) + bob <# ("", aliceConn, CON) + + alice #: ("3", bobConn, "SEND :hi") #> ("3", bobConn, MID 1) + alice <# ("", bobConn, SENT 1) + bob <#= \case ("", c, Msg "hi") -> c == aliceConn; _ -> False + bob #: ("13", aliceConn, "ACK 1") #> ("13", aliceConn, OK) + testSubscription :: Transport c => TProxy c -> c -> c -> c -> IO () testSubscription _ alice1 alice2 bob = do (alice1, "alice") `connect` (bob, "bob") @@ -185,7 +244,7 @@ testSubscription _ alice1 alice2 bob = do testSubscrNotification :: Transport c => TProxy c -> (ThreadId, ThreadId) -> c -> IO () testSubscrNotification t (server, _) client = do - client #: ("1", "conn1", "NEW") =#> \case ("1", "conn1", INV {}) -> True; _ -> False + client #: ("1", "conn1", "NEW INV") =#> \case ("1", "conn1", INV {}) -> True; _ -> False client #:# "nothing should be delivered to client before the server is killed" killThread server client <# ("", "conn1", DOWN) @@ -253,18 +312,18 @@ testMsgDeliveryAgentRestart t bob = do connect :: forall c. Transport c => (c, ByteString) -> (c, ByteString) -> IO () connect (h1, name1) (h2, name2) = do - ("c1", _, Right (INV cReq)) <- h1 #: ("c1", name2, "NEW") + ("c1", _, Right (INV cReq)) <- h1 #: ("c1", name2, "NEW INV") let cReq' = serializeConnReq cReq h2 #: ("c2", name1, "JOIN " <> cReq' <> " 5\ninfo2") #> ("c2", name1, OK) - ("", _, Right (REQ connId "info2")) <- (h1 <#:) - h1 #: ("c3", name2, "ACPT " <> connId <> " 5\ninfo1") #> ("c3", name2, OK) + ("", _, Right (REQ_INV connId "info2")) <- (h1 <#:) + h1 #: ("c3", name2, "ACPT INV " <> connId <> " 5\ninfo1") #> ("c3", name2, OK) h2 <# ("", name1, INFO "info1") h2 <# ("", name1, CON) h1 <# ("", name2, CON) -- connect' :: forall c. Transport c => c -> c -> IO (ByteString, ByteString) -- connect' h1 h2 = do --- ("c1", conn2, Right (INV cReq)) <- h1 #: ("c1", "", "NEW") +-- ("c1", conn2, Right (INV cReq)) <- h1 #: ("c1", "", "NEW INV") -- let cReq' = serializeConnReq cReq -- ("c2", conn1, Right OK) <- h2 #: ("c2", "", "JOIN " <> cReq' <> " 5\ninfo2") -- ("", _, Right (REQ connId "info2")) <- (h1 <#:) @@ -283,17 +342,17 @@ syntaxTests t = do describe "NEW" do describe "valid" do -- TODO: add tests with defined connection alias - it "without parameters" $ ("211", "", "NEW") >#>= \case ("211", _, "INV" : _) -> True; _ -> False + it "with correct parameter" $ ("211", "", "NEW INV") >#>= \case ("211", _, "INV" : _) -> True; _ -> False describe "invalid" do -- TODO: add tests with defined connection alias - it "with parameters" $ ("222", "", "NEW hi") >#> ("222", "", "ERR CMD SYNTAX") + it "with incorrect parameter" $ ("222", "", "NEW hi") >#> ("222", "", "ERR CMD SYNTAX") 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", "JOIN https://simpex.chat/connect#/?smp=smp%3A%2F%2Flocalhost%3A5000%2F1234-w%3D%3D%23&e2e=" <> urlEncode True samplePublicKey <> " 14\nbob's connInfo") >#> ("311", "a", "ERR SMP AUTH") + ("311", "a", "JOIN https://simpex.chat/invitation#/?smp=smp%3A%2F%2Flocalhost%3A5000%2F1234-w%3D%3D%23&e2e=" <> urlEncode True samplePublicKey <> " 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") diff --git a/tests/AgentTests/ConnectionRequestTests.hs b/tests/AgentTests/ConnectionRequestTests.hs index 2355b5931..a39c54ce1 100644 --- a/tests/AgentTests/ConnectionRequestTests.hs +++ b/tests/AgentTests/ConnectionRequestTests.hs @@ -30,14 +30,14 @@ queue = appServer :: ConnReqScheme appServer = CRSAppServer "simplex.chat" Nothing -connReq :: ConnectionRequest -connReq = - ConnectionRequest - { crScheme = appServer, - crAction = CRAConnect, - crSmpQueues = [queue], - crEncryptKey = reservedServerKey - } +connectionRequest :: AConnectionRequest +connectionRequest = + ACR SCMInvitation . CRInvitation $ + ConnReqData + { crScheme = appServer, + crSmpQueues = [queue], + crEncryptKey = reservedServerKey + } connectionRequestTests :: Spec connectionRequestTests = do @@ -61,9 +61,8 @@ connectionRequestTests = do parseAll smpQueueUriP "smp://1234-w==@smp.simplex.im:5223/1234-w==#" `shouldBe` Right queue it "should serialize connection requests" $ do - serializeConnReq connReq - `shouldBe` "https://simplex.chat/connect#/?smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F1234-w%3D%3D%23&e2e=rsa%3AMBowDQYJKoZIhvcNAQEBBQADCQAwBgIBAAIBAA%3D%3D" + serializeConnReq connectionRequest + `shouldBe` "https://simplex.chat/invitation#/?smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F1234-w%3D%3D%23&e2e=rsa%3AMBowDQYJKoZIhvcNAQEBBQADCQAwBgIBAAIBAA%3D%3D" it "should parse connection requests" $ do - -- print $ parseSMPMessage "\n0 2021-11-29T19:23:27.005Z \nREPLY simplex:/connect#/?smp=smp%3A%2F%2FKXNE1m2E1m0lm92WGKet9CL6-lO742Vy5G6nsrkvgs8%3D%40localhost%3A5000%2FoWWCE_5ug0t05K6X%23&e2e=rsa%3AMIIBoDANBgkqhkiG9w0BAQEFAAOCAY0AMIIBiAKCAQEA3O4frbgUMRO%2B8FIX2%2ByqB%2F1B5pXmt%2F%2FY0dFd2HCVxL31TJHc90HJp92Qb7Ni%2B1dI2Ka1Hb1Fvup897mmEcFhZStG0OB6jffvPyxXCas8Tov3l757qCUZKqgTxSJkL7JvLkIN9jMs50islvrSHCAj8VReh5oR%2B8OFp8ITd5MuMHYuR1bt0XLl1TwSIyfRSQqtHlt%2FEBbEbWcgJMsDXMi3o983nezvF9En9F7OCnasdzKAsgcN2%2FdWp3CPeuMNe9epzrirxGfCKU%2FlVyZ77e7NZMkSmeOIDPGuE4Fk8bweAYArV%2FrECBJGBQkGx3YtEh0kIbCakQ1ZnKY%2F%2FMq0ZHGPhQKBgGRKfJ7xXftoLdVJ7EOW%2FR5Y%2Bj%2F%2Bb9yZMbTCdZfkuroV9FH8GF5tS3PWuSAOFu42h7TiqFjXlvM6aYp%2FBXxCosZjBlB6mWCLyuY48ZszhtCpLSlbR2x%2FpGMUEgyOsefeMusrHEqFJAI%2Fhh8LljBGL%2BV08qcGFxVTwCVePIjDOo1H\n" - parseAll connReqP "https://simplex.chat/connect#/?smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F1234-w%3D%3D%23&e2e=rsa%3AMBowDQYJKoZIhvcNAQEBBQADCQAwBgIBAAIBAA%3D%3D" - `shouldBe` Right connReq + parseAll connReqP "https://simplex.chat/invitation#/?smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F1234-w%3D%3D%23&e2e=rsa%3AMBowDQYJKoZIhvcNAQEBBQADCQAwBgIBAAIBAA%3D%3D" + `shouldBe` Right connectionRequest diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index 008e0c14b..d0c701e56 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -5,7 +5,12 @@ {-# LANGUAGE PatternSynonyms #-} {-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-} -module AgentTests.FunctionalAPITests (functionalAPITests) where +module AgentTests.FunctionalAPITests + ( functionalAPITests, + pattern REQ_INV, + pattern REQ_CON, + ) +where import Control.Monad.Except (ExceptT, runExceptT) import Control.Monad.IO.Unlift @@ -32,6 +37,12 @@ get c = atomically (readTBQueue $ subQ c) pattern Msg :: MsgBody -> ACommand 'Agent pattern Msg msgBody <- MSG MsgMeta {integrity = MsgOk} msgBody +pattern REQ_INV :: ConfirmationId -> ConnInfo -> ACommand 'Agent +pattern REQ_INV confId cInfo <- REQ (ACM SCMInvitation) confId cInfo + +pattern REQ_CON :: ConfirmationId -> ConnInfo -> ACommand 'Agent +pattern REQ_CON confId cInfo <- REQ (ACM SCMContact) confId cInfo + functionalAPITests :: ATransport -> Spec functionalAPITests t = do describe "Establishing duplex connection" $ @@ -53,9 +64,9 @@ testAgentClient = do alice <- getSMPAgentClient cfg bob <- getSMPAgentClient cfg {dbFile = testDB2} Right () <- runExceptT $ do - (bobId, qInfo) <- createConnection alice + (bobId, qInfo) <- createConnection alice SCMInvitation aliceId <- joinConnection bob qInfo "bob's connInfo" - ("", _, REQ confId "bob's connInfo") <- get alice + ("", _, REQ_INV confId "bob's connInfo") <- get alice acceptConnection alice bobId confId "alice's connInfo" get alice ##> ("", bobId, CON) get bob ##> ("", aliceId, INFO "alice's connInfo") @@ -96,12 +107,12 @@ testAsyncInitiatingOffline = do alice <- getSMPAgentClient cfg bob <- getSMPAgentClient cfg {dbFile = testDB2} Right () <- runExceptT $ do - (bobId, qInfo) <- createConnection alice + (bobId, cReq) <- createConnection alice SCMInvitation disconnectAgentClient alice - aliceId <- joinConnection bob qInfo "bob's connInfo" + aliceId <- joinConnection bob cReq "bob's connInfo" alice' <- liftIO $ getSMPAgentClient cfg subscribeConnection alice' bobId - ("", _, REQ confId "bob's connInfo") <- get alice' + ("", _, REQ_INV confId "bob's connInfo") <- get alice' acceptConnection alice' bobId confId "alice's connInfo" get alice' ##> ("", bobId, CON) get bob ##> ("", aliceId, INFO "alice's connInfo") @@ -114,10 +125,10 @@ testAsyncJoiningOfflineBeforeActivation = do alice <- getSMPAgentClient cfg bob <- getSMPAgentClient cfg {dbFile = testDB2} Right () <- runExceptT $ do - (bobId, qInfo) <- createConnection alice + (bobId, qInfo) <- createConnection alice SCMInvitation aliceId <- joinConnection bob qInfo "bob's connInfo" disconnectAgentClient bob - ("", _, REQ confId "bob's connInfo") <- get alice + ("", _, REQ_INV confId "bob's connInfo") <- get alice acceptConnection alice bobId confId "alice's connInfo" bob' <- liftIO $ getSMPAgentClient cfg {dbFile = testDB2} subscribeConnection bob' aliceId @@ -135,13 +146,13 @@ testAsyncBothOffline = do alice <- getSMPAgentClient cfg bob <- getSMPAgentClient cfg {dbFile = testDB2} Right () <- runExceptT $ do - (bobId, qInfo) <- createConnection alice + (bobId, cReq) <- createConnection alice SCMInvitation disconnectAgentClient alice - aliceId <- joinConnection bob qInfo "bob's connInfo" + aliceId <- joinConnection bob cReq "bob's connInfo" disconnectAgentClient bob alice' <- liftIO $ getSMPAgentClient cfg subscribeConnection alice' bobId - ("", _, REQ confId "bob's connInfo") <- get alice' + ("", _, REQ_INV confId "bob's connInfo") <- get alice' acceptConnection alice' bobId confId "alice's connInfo" bob' <- liftIO $ getSMPAgentClient cfg {dbFile = testDB2} subscribeConnection bob' aliceId diff --git a/tests/AgentTests/SQLiteTests.hs b/tests/AgentTests/SQLiteTests.hs index 26d652ad9..725367235 100644 --- a/tests/AgentTests/SQLiteTests.hs +++ b/tests/AgentTests/SQLiteTests.hs @@ -114,7 +114,7 @@ testConcurrentWrites :: SpecWith (SQLiteStore, SQLiteStore) testConcurrentWrites = it "should complete multiple concurrent write transactions w/t sqlite busy errors" $ \(s1, s2) -> do g <- newTVarIO =<< drgNew - _ <- runExceptT $ createRcvConn s1 g cData1 rcvQueue1 + _ <- runExceptT $ createRcvConn s1 g cData1 rcvQueue1 SCMInvitation let ConnData {connId} = cData1 concurrently_ (runTest s1 connId) (runTest s2 connId) where @@ -176,7 +176,7 @@ testCreateRcvConn :: SpecWith SQLiteStore testCreateRcvConn = it "should create RcvConnection and add SndQueue" $ \store -> do g <- newTVarIO =<< drgNew - createRcvConn store g cData1 rcvQueue1 + createRcvConn store g cData1 rcvQueue1 SCMInvitation `returnsResult` "conn1" getConn store "conn1" `returnsResult` SomeConn SCRcv (RcvConnection cData1 rcvQueue1) @@ -189,7 +189,7 @@ testCreateRcvConnRandomId :: SpecWith SQLiteStore testCreateRcvConnRandomId = it "should create RcvConnection and add SndQueue with random ID" $ \store -> do g <- newTVarIO =<< drgNew - Right connId <- runExceptT $ createRcvConn store g cData1 {connId = ""} rcvQueue1 + Right connId <- runExceptT $ createRcvConn store g cData1 {connId = ""} rcvQueue1 SCMInvitation getConn store connId `returnsResult` SomeConn SCRcv (RcvConnection cData1 {connId} rcvQueue1) upgradeRcvConnToDuplex store connId sndQueue1 @@ -201,8 +201,8 @@ testCreateRcvConnDuplicate :: SpecWith SQLiteStore testCreateRcvConnDuplicate = it "should throw error on attempt to create duplicate RcvConnection" $ \store -> do g <- newTVarIO =<< drgNew - _ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 - createRcvConn store g cData1 rcvQueue1 + _ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 SCMInvitation + createRcvConn store g cData1 rcvQueue1 SCMInvitation `throwsError` SEConnDuplicate testCreateSndConn :: SpecWith SQLiteStore @@ -242,7 +242,7 @@ testGetAllConnIds :: SpecWith SQLiteStore testGetAllConnIds = it "should get all conn aliases" $ \store -> do g <- newTVarIO =<< drgNew - _ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 + _ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 SCMInvitation _ <- runExceptT $ createSndConn store g cData1 {connId = "conn2"} sndQueue1 getAllConnIds store `returnsResult` ["conn1" :: ConnId, "conn2" :: ConnId] @@ -253,7 +253,7 @@ testGetRcvConn = let smpServer = SMPServer "smp.simplex.im" (Just "5223") testKeyHash let recipientId = "1234" g <- newTVarIO =<< drgNew - _ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 + _ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 SCMInvitation getRcvConn store smpServer recipientId `returnsResult` SomeConn SCRcv (RcvConnection cData1 rcvQueue1) @@ -261,7 +261,7 @@ testDeleteRcvConn :: SpecWith SQLiteStore testDeleteRcvConn = it "should create RcvConnection and delete it" $ \store -> do g <- newTVarIO =<< drgNew - _ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 + _ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 SCMInvitation getConn store "conn1" `returnsResult` SomeConn SCRcv (RcvConnection cData1 rcvQueue1) deleteConn store "conn1" @@ -287,7 +287,7 @@ testDeleteDuplexConn :: SpecWith SQLiteStore testDeleteDuplexConn = it "should create DuplexConnection and delete it" $ \store -> do g <- newTVarIO =<< drgNew - _ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 + _ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 SCMInvitation _ <- runExceptT $ upgradeRcvConnToDuplex store "conn1" sndQueue1 getConn store "conn1" `returnsResult` SomeConn SCDuplex (DuplexConnection cData1 rcvQueue1 sndQueue1) @@ -321,7 +321,7 @@ testUpgradeSndConnToDuplex :: SpecWith SQLiteStore testUpgradeSndConnToDuplex = it "should throw error on attempt to add RcvQueue to RcvConnection or DuplexConnection" $ \store -> do g <- newTVarIO =<< drgNew - _ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 + _ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 SCMInvitation let anotherRcvQueue = RcvQueue { server = SMPServer "smp.simplex.im" (Just "5223") testKeyHash, @@ -342,7 +342,7 @@ testSetRcvQueueStatus :: SpecWith SQLiteStore testSetRcvQueueStatus = it "should update status of RcvQueue" $ \store -> do g <- newTVarIO =<< drgNew - _ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 + _ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 SCMInvitation getConn store "conn1" `returnsResult` SomeConn SCRcv (RcvConnection cData1 rcvQueue1) setRcvQueueStatus store rcvQueue1 Confirmed @@ -366,7 +366,7 @@ testSetQueueStatusDuplex :: SpecWith SQLiteStore testSetQueueStatusDuplex = it "should update statuses of RcvQueue and SndQueue in DuplexConnection" $ \store -> do g <- newTVarIO =<< drgNew - _ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 + _ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 SCMInvitation _ <- runExceptT $ upgradeRcvConnToDuplex store "conn1" sndQueue1 getConn store "conn1" `returnsResult` SomeConn SCDuplex (DuplexConnection cData1 rcvQueue1 sndQueue1) @@ -426,7 +426,7 @@ testCreateRcvMsg = it "should reserve internal ids and create a RcvMsg" $ \st -> do g <- newTVarIO =<< drgNew let ConnData {connId} = cData1 - _ <- runExceptT $ createRcvConn st g cData1 rcvQueue1 + _ <- 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" @@ -464,7 +464,7 @@ testCreateRcvAndSndMsgs = it "should create multiple RcvMsg and SndMsg, correctly ordering internal Ids and returning previous state" $ \store -> do g <- newTVarIO =<< drgNew let ConnData {connId} = cData1 - _ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 + _ <- 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" From 3e226fc3f2c1b2cabb4e22e77818cbe41ae93b7a Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 4 Dec 2021 21:08:02 +0000 Subject: [PATCH 13/20] add CONF/LET for invitations, use REQ/ACPT only with contact connections (#212) --- protocol/agent-protocol.md | 27 ++++++++++++++---- src/Simplex/Messaging/Agent.hs | 19 ++++++------- src/Simplex/Messaging/Agent/Protocol.hs | 30 ++++++++++++------- tests/AgentTests.hs | 38 ++++++++++++------------- tests/AgentTests/FunctionalAPITests.hs | 29 ++++++------------- 5 files changed, 77 insertions(+), 66 deletions(-) diff --git a/protocol/agent-protocol.md b/protocol/agent-protocol.md index 033763047..011fd608b 100644 --- a/protocol/agent-protocol.md +++ b/protocol/agent-protocol.md @@ -20,6 +20,7 @@ - [Client commands and server responses](#client-commands-and-server-responses) - [NEW command and INV response](#new-command-and-inv-response) - [JOIN command](#join-command) + - [CONF notification and LET command](#conf-notification-and-let-command) - [REQ notification and ACPT command](#req-notification-and-acpt-command) - [INFO and CON notifications](#info-and-con-notifications) - [SUB command](#sub-command) @@ -233,8 +234,8 @@ cId = encoded cName = 1*(ALPHA / DIGIT / "_" / "-") agentCommand = (userCmd / agentMsg) CRLF -userCmd = newCmd / joinCmd / acceptCmd / subscribeCmd / sendCmd / acknowledgeCmd / suspendCmd / deleteCmd -agentMsg = invitation / connRequest / connInfo / connected / unsubscribed / connDown / connUp / messageId / sent / messageError / message / received / ok / error +userCmd = newCmd / joinCmd / letCmd / acceptCmd / subscribeCmd / sendCmd / acknowledgeCmd / suspendCmd / deleteCmd +agentMsg = invitation / confMsg / connReqMsg / connInfo / connected / unsubscribed / connDown / connUp / messageId / sent / messageError / message / received / ok / error newCmd = %s"NEW" SP connectionMode [SP %s"NO_ACK"] ; response is `invitation` or `error` ; NO_ACK parameter currently not supported @@ -243,14 +244,22 @@ connectionMode = %s"INV" / %s"CON" invitation = %s"INV" SP connectionRequest ; `connectionRequest` is defined below -connRequest = %s"REQ" SP connectionMode SP confirmationId SP msgBody +confMsg = %s"CONF" SP confirmationId SP msgBody ; msgBody here is any binary information identifying connection request +letCmd = %s"LET" SP confirmationId SP msgBody +; msgBody here is any binary information identifying connecting party + confirmationId = 1*DIGIT -acceptCmd = %s"ACPT" SP connectionMode SP confirmationId SP msgBody +connReqMsg = %s"REQ" SP invitationId SP msgBody +; msgBody here is any binary information identifying connection request + +acceptCmd = %s"ACPT" SP invitationId SP msgBody ; msgBody here is any binary information identifying connecting party +invitationId = 1*DIGIT + connInfo = %s"INFO" SP msgBody ; msgBody here is any binary information identifying connecting party @@ -338,11 +347,17 @@ error = %s"ERR" SP It is used to create a connection and accept the connection request received out-of-band. It should be used by the client of the agent that accepts the connection (the joining party). +#### CONF notification and LET command + +When the joining party uses `JOIN` command to accept connection invitation created with `NEW INV` command, the initiating party will receive `CONF` notification with some numeric identifier and an additional binary information, that can be used to identify the joining party or for any other purpose. + +To continue with the connection the initiating party should use `LET` command. + #### REQ notification and ACPT command -When the joining party uses `JOIN` command, the initiating party will receive `REQ` notification with some numeric identifier and an additional binary information, that can be used to identify the joining party or for any other purpose. +When the joining party uses `JOIN` command to connect to the contact created with `NEW CON` command, the initiating party will receive `REQ` notification with some numeric identifier and an additional binary information, that can be used to identify the joining party or for any other purpose. -To continue with the connection the initiating party should use `ACPT` command. +To continue with the connection the party that created the contact should use `ACPT` command. #### INFO and CON notifications diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 81b16f2ac..ef3ed45d2 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -45,7 +45,7 @@ module Simplex.Messaging.Agent withAgentLock, createConnection, joinConnection, - acceptConnection, + allowConnection, acceptContact, subscribeConnection, sendMessage, @@ -139,8 +139,8 @@ joinConnection :: AgentErrorMonad m => AgentClient -> ConnectionRequest c -> Con joinConnection c = withAgentEnv c .: joinConn c "" -- | Approve confirmation (ACPT INV command) -acceptConnection :: AgentErrorMonad m => AgentClient -> ConnId -> ConfirmationId -> ConnInfo -> m () -acceptConnection c = withAgentEnv c .:. acceptConnection' c +allowConnection :: AgentErrorMonad m => AgentClient -> ConnId -> ConfirmationId -> ConnInfo -> m () +allowConnection c = withAgentEnv c .:. allowConnection' c -- | Approve contact (ACPT CON command) acceptContact :: AgentErrorMonad m => AgentClient -> ConfirmationId -> ConnInfo -> m ConnId @@ -244,9 +244,8 @@ processCommand :: forall m. AgentMonad m => AgentClient -> (ConnId, ACommand 'Cl 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 - ACPT (ACM cMode) confInvId ownConnInfo -> case cMode of - SCMInvitation -> acceptConnection' c connId confInvId ownConnInfo $> (connId, OK) - SCMContact -> (,OK) <$> acceptContact' c connId confInvId ownConnInfo + LET confId ownCInfo -> allowConnection' c connId confId ownCInfo $> (connId, OK) + ACPT invId ownCInfo -> (,OK) <$> acceptContact' c connId invId ownCInfo SUB -> subscribeConnection' c connId $> (connId, OK) SEND msgBody -> (connId,) . MID <$> sendMessage' c connId msgBody ACK msgId -> ackMessage' c connId msgId $> (connId, OK) @@ -294,8 +293,8 @@ activateQueueJoining c connId sq verifyKey retryInterval = sendControlMessage c sq . REPLY $ CRInvitation $ ConnReqData CRSSimplex [qUri'] encryptKey -- | Approve confirmation (ACPT INV command) in Reader monad -acceptConnection' :: AgentMonad m => AgentClient -> ConnId -> ConfirmationId -> ConnInfo -> m () -acceptConnection' c connId confId ownConnInfo = do +allowConnection' :: AgentMonad m => AgentClient -> ConnId -> ConfirmationId -> ConnInfo -> m () +allowConnection' c connId confId ownConnInfo = do withStore (`getConn` connId) >>= \case SomeConn _ (RcvConnection _ rq) -> do AcceptedConfirmation {senderKey} <- withStore $ \st -> acceptConfirmation st confId ownConnInfo @@ -567,7 +566,7 @@ processSMPTransmission c@AgentClient {subQ} (srv, rId, cmd) = do g <- asks idsDrg let newConfirmation = NewConfirmation {connId, senderKey, senderConnInfo = cInfo} confId <- withStore $ \st -> createConfirmation st g newConfirmation - notify $ REQ cmInvitation confId cInfo + notify $ CONF confId cInfo SCDuplex -> do notify $ INFO cInfo processConfirmation c rq senderKey @@ -620,7 +619,7 @@ processSMPTransmission c@AgentClient {subQ} (srv, rId, cmd) = do g <- asks idsDrg let newInv = NewInvitation {contactConnId = connId, connReq, recipientConnInfo = cInfo} invId <- withStore $ \st -> createInvitation st g newInv - notify $ REQ cmContact invId cInfo + notify $ REQ invId cInfo _ -> prohibited checkMsgIntegrity :: PrevExternalSndId -> ExternalSndId -> PrevRcvMsgHash -> ByteString -> MsgIntegrity diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index 706b638af..6c0c322d3 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -186,8 +186,10 @@ data ACommand (p :: AParty) where NEW :: AConnectionMode -> ACommand Client -- response INV INV :: AConnectionRequest -> ACommand Agent JOIN :: AConnectionRequest -> ConnInfo -> ACommand Client -- response OK - REQ :: AConnectionMode -> ConfOrInvId -> ConnInfo -> ACommand Agent -- ConnInfo is from sender - ACPT :: AConnectionMode -> ConfOrInvId -> ConnInfo -> ACommand Client -- ConnInfo is from client + 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 + ACPT :: InvitationId -> ConnInfo -> ACommand Client -- ConnInfo is from client INFO :: ConnInfo -> ACommand Agent CON :: ACommand Agent -- notification that connection is established SUB :: ACommand Client @@ -487,8 +489,6 @@ type ConfirmationId = ByteString type InvitationId = ByteString -type ConfOrInvId = ByteString - -- | Connection modes. data OnOff = On | Off deriving (Eq, Show, Read) @@ -657,7 +657,9 @@ commandP = "NEW " *> newCmd <|> "INV " *> invResp <|> "JOIN " *> joinCmd - <|> "REQ " *> reqCmd + <|> "CONF " *> confMsg + <|> "LET " *> letCmd + <|> "REQ " *> reqMsg <|> "ACPT " *> acptCmd <|> "INFO " *> infoCmd <|> "SUB" $> ACmd SClient SUB @@ -679,8 +681,10 @@ commandP = newCmd = ACmd SClient . NEW <$> connModeP invResp = ACmd SAgent . INV <$> connReqP joinCmd = ACmd SClient <$> (JOIN <$> connReqP <* A.space <*> A.takeByteString) - reqCmd = ACmd SAgent <$> (REQ <$> connModeP <* A.space <*> A.takeTill (== ' ') <* A.space <*> A.takeByteString) - acptCmd = ACmd SClient <$> (ACPT <$> connModeP <* A.space <*> A.takeTill (== ' ') <* A.space <*> A.takeByteString) + confMsg = ACmd SAgent <$> (CONF <$> A.takeTill (== ' ') <* A.space <*> A.takeByteString) + letCmd = ACmd SClient <$> (LET <$> A.takeTill (== ' ') <* A.space <*> A.takeByteString) + reqMsg = ACmd SAgent <$> (REQ <$> A.takeTill (== ' ') <* A.space <*> A.takeByteString) + acptCmd = ACmd SClient <$> (ACPT <$> A.takeTill (== ' ') <* A.space <*> A.takeByteString) infoCmd = ACmd SAgent . INFO <$> A.takeByteString sendCmd = ACmd SClient . SEND <$> A.takeByteString msgIdResp = ACmd SAgent . MID <$> A.decimal @@ -716,8 +720,10 @@ serializeCommand = \case NEW cMode -> "NEW " <> serializeConnMode cMode INV cReq -> "INV " <> serializeConnReq cReq JOIN cReq cInfo -> B.unwords ["JOIN", serializeConnReq cReq, serializeBinary cInfo] - REQ cMode confId cInfo -> B.unwords ["REQ", serializeConnMode cMode, confId, serializeBinary cInfo] - ACPT cMode confId cInfo -> B.unwords ["ACPT", serializeConnMode cMode, confId, serializeBinary cInfo] + CONF confId cInfo -> B.unwords ["CONF", confId, serializeBinary cInfo] + LET confId cInfo -> B.unwords ["LET", confId, serializeBinary cInfo] + REQ invId cInfo -> B.unwords ["REQ", invId, serializeBinary cInfo] + ACPT invId cInfo -> B.unwords ["ACPT", invId, serializeBinary cInfo] INFO cInfo -> "INFO " <> serializeBinary cInfo SUB -> "SUB" END -> "END" @@ -832,8 +838,10 @@ tGet party h = liftIO (tGetRaw h) >>= tParseLoadBody SEND body -> SEND <$$> getBody body MSG msgMeta body -> MSG msgMeta <$$> getBody body JOIN qUri cInfo -> JOIN qUri <$$> getBody cInfo - REQ cMode confId cInfo -> REQ cMode confId <$$> getBody cInfo - ACPT cMode confId cInfo -> ACPT cMode confId <$$> getBody cInfo + CONF confId cInfo -> CONF confId <$$> getBody cInfo + LET confId cInfo -> LET confId <$$> getBody cInfo + REQ invId cInfo -> REQ invId <$$> getBody cInfo + ACPT invId cInfo -> ACPT invId <$$> getBody cInfo INFO cInfo -> INFO <$$> getBody cInfo cmd -> pure $ Right cmd diff --git a/tests/AgentTests.hs b/tests/AgentTests.hs index fc115b6d4..4b8dcd95c 100644 --- a/tests/AgentTests.hs +++ b/tests/AgentTests.hs @@ -11,7 +11,7 @@ module AgentTests (agentTests) where import AgentTests.ConnectionRequestTests -import AgentTests.FunctionalAPITests (functionalAPITests, pattern REQ_CON, pattern REQ_INV) +import AgentTests.FunctionalAPITests (functionalAPITests) import AgentTests.SQLiteTests (storeTests) import Control.Concurrent import Data.ByteString.Char8 (ByteString) @@ -109,8 +109,8 @@ testDuplexConnection _ alice bob = do ("1", "bob", Right (INV cReq)) <- alice #: ("1", "bob", "NEW INV") let cReq' = serializeConnReq cReq bob #: ("11", "alice", "JOIN " <> cReq' <> " 14\nbob's connInfo") #> ("11", "alice", OK) - ("", "bob", Right (REQ_INV confId "bob's connInfo")) <- (alice <#:) - alice #: ("2", "bob", "ACPT INV " <> confId <> " 16\nalice's connInfo") #> ("2", "bob", OK) + ("", "bob", Right (CONF confId "bob's connInfo")) <- (alice <#:) + alice #: ("2", "bob", "LET " <> confId <> " 16\nalice's connInfo") #> ("2", "bob", OK) bob <# ("", "alice", INFO "alice's connInfo") bob <# ("", "alice", CON) alice <# ("", "bob", CON) @@ -141,9 +141,9 @@ testDuplexConnRandomIds _ alice bob = do ("1", bobConn, Right (INV cReq)) <- alice #: ("1", "", "NEW INV") let cReq' = serializeConnReq cReq ("11", aliceConn, Right OK) <- bob #: ("11", "", "JOIN " <> cReq' <> " 14\nbob's connInfo") - ("", bobConn', Right (REQ_INV confId "bob's connInfo")) <- (alice <#:) + ("", bobConn', Right (CONF confId "bob's connInfo")) <- (alice <#:) bobConn' `shouldBe` bobConn - alice #: ("2", bobConn, "ACPT INV " <> confId <> " 16\nalice's connInfo") =#> \case ("2", c, OK) -> c == bobConn; _ -> False + alice #: ("2", bobConn, "LET " <> confId <> " 16\nalice's connInfo") =#> \case ("2", c, OK) -> c == bobConn; _ -> False bob <# ("", aliceConn, INFO "alice's connInfo") bob <# ("", aliceConn, CON) alice <# ("", bobConn, CON) @@ -175,10 +175,10 @@ testContactConnection _ alice bob tom = do let cReq' = serializeConnReq cReq bob #: ("11", "alice", "JOIN " <> cReq' <> " 14\nbob's connInfo") #> ("11", "alice", OK) - ("", "alice_contact", Right (REQ_CON aConfId "bob's connInfo")) <- (alice <#:) - alice #: ("2", "bob", "ACPT CON " <> aConfId <> " 16\nalice's connInfo") #> ("2", "bob", OK) - ("", "alice", Right (REQ_INV bConfId "alice's connInfo")) <- (bob <#:) - bob #: ("12", "alice", "ACPT INV " <> bConfId <> " 16\nbob's connInfo 2") #> ("12", "alice", OK) + ("", "alice_contact", Right (REQ aConfId "bob's connInfo")) <- (alice <#:) + alice #: ("2", "bob", "ACPT " <> aConfId <> " 16\nalice's connInfo") #> ("2", "bob", OK) + ("", "alice", Right (CONF bConfId "alice's connInfo")) <- (bob <#:) + bob #: ("12", "alice", "LET " <> bConfId <> " 16\nbob's connInfo 2") #> ("12", "alice", OK) alice <# ("", "bob", INFO "bob's connInfo 2") alice <# ("", "bob", CON) bob <# ("", "alice", CON) @@ -188,10 +188,10 @@ testContactConnection _ alice bob tom = do bob #: ("13", "alice", "ACK 1") #> ("13", "alice", OK) tom #: ("21", "alice", "JOIN " <> cReq' <> " 14\ntom's connInfo") #> ("21", "alice", OK) - ("", "alice_contact", Right (REQ_CON aConfId' "tom's connInfo")) <- (alice <#:) - alice #: ("4", "tom", "ACPT CON " <> aConfId' <> " 16\nalice's connInfo") #> ("4", "tom", OK) - ("", "alice", Right (REQ_INV tConfId "alice's connInfo")) <- (tom <#:) - tom #: ("22", "alice", "ACPT INV " <> tConfId <> " 16\ntom's connInfo 2") #> ("22", "alice", OK) + ("", "alice_contact", Right (REQ aConfId' "tom's connInfo")) <- (alice <#:) + alice #: ("4", "tom", "ACPT " <> aConfId' <> " 16\nalice's connInfo") #> ("4", "tom", OK) + ("", "alice", Right (CONF tConfId "alice's connInfo")) <- (tom <#:) + tom #: ("22", "alice", "LET " <> tConfId <> " 16\ntom's connInfo 2") #> ("22", "alice", OK) alice <# ("", "tom", INFO "tom's connInfo 2") alice <# ("", "tom", CON) tom <# ("", "alice", CON) @@ -206,14 +206,14 @@ testContactConnRandomIds _ alice bob = do let cReq' = serializeConnReq cReq ("11", aliceConn, Right OK) <- bob #: ("11", "", "JOIN " <> cReq' <> " 14\nbob's connInfo") - ("", aliceContact', Right (REQ_CON aConfId "bob's connInfo")) <- (alice <#:) + ("", aliceContact', Right (REQ aConfId "bob's connInfo")) <- (alice <#:) aliceContact' `shouldBe` aliceContact - ("2", bobConn, Right OK) <- alice #: ("2", "", "ACPT CON " <> aConfId <> " 16\nalice's connInfo") - ("", aliceConn', Right (REQ_INV bConfId "alice's connInfo")) <- (bob <#:) + ("2", bobConn, Right OK) <- alice #: ("2", "", "ACPT " <> aConfId <> " 16\nalice's connInfo") + ("", aliceConn', Right (CONF bConfId "alice's connInfo")) <- (bob <#:) aliceConn' `shouldBe` aliceConn - bob #: ("12", aliceConn, "ACPT INV " <> bConfId <> " 16\nbob's connInfo 2") #> ("12", aliceConn, OK) + bob #: ("12", aliceConn, "LET " <> bConfId <> " 16\nbob's connInfo 2") #> ("12", aliceConn, OK) alice <# ("", bobConn, INFO "bob's connInfo 2") alice <# ("", bobConn, CON) bob <# ("", aliceConn, CON) @@ -315,8 +315,8 @@ connect (h1, name1) (h2, name2) = do ("c1", _, Right (INV cReq)) <- h1 #: ("c1", name2, "NEW INV") let cReq' = serializeConnReq cReq h2 #: ("c2", name1, "JOIN " <> cReq' <> " 5\ninfo2") #> ("c2", name1, OK) - ("", _, Right (REQ_INV connId "info2")) <- (h1 <#:) - h1 #: ("c3", name2, "ACPT INV " <> connId <> " 5\ninfo1") #> ("c3", name2, OK) + ("", _, Right (CONF connId "info2")) <- (h1 <#:) + h1 #: ("c3", name2, "LET " <> connId <> " 5\ninfo1") #> ("c3", name2, OK) h2 <# ("", name1, INFO "info1") h2 <# ("", name1, CON) h1 <# ("", name2, CON) diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index d0c701e56..b37d0cbd3 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -5,12 +5,7 @@ {-# LANGUAGE PatternSynonyms #-} {-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-} -module AgentTests.FunctionalAPITests - ( functionalAPITests, - pattern REQ_INV, - pattern REQ_CON, - ) -where +module AgentTests.FunctionalAPITests (functionalAPITests) where import Control.Monad.Except (ExceptT, runExceptT) import Control.Monad.IO.Unlift @@ -37,12 +32,6 @@ get c = atomically (readTBQueue $ subQ c) pattern Msg :: MsgBody -> ACommand 'Agent pattern Msg msgBody <- MSG MsgMeta {integrity = MsgOk} msgBody -pattern REQ_INV :: ConfirmationId -> ConnInfo -> ACommand 'Agent -pattern REQ_INV confId cInfo <- REQ (ACM SCMInvitation) confId cInfo - -pattern REQ_CON :: ConfirmationId -> ConnInfo -> ACommand 'Agent -pattern REQ_CON confId cInfo <- REQ (ACM SCMContact) confId cInfo - functionalAPITests :: ATransport -> Spec functionalAPITests t = do describe "Establishing duplex connection" $ @@ -66,8 +55,8 @@ testAgentClient = do Right () <- runExceptT $ do (bobId, qInfo) <- createConnection alice SCMInvitation aliceId <- joinConnection bob qInfo "bob's connInfo" - ("", _, REQ_INV confId "bob's connInfo") <- get alice - acceptConnection alice bobId confId "alice's connInfo" + ("", _, CONF confId "bob's connInfo") <- get alice + allowConnection alice bobId confId "alice's connInfo" get alice ##> ("", bobId, CON) get bob ##> ("", aliceId, INFO "alice's connInfo") get bob ##> ("", aliceId, CON) @@ -112,8 +101,8 @@ testAsyncInitiatingOffline = do aliceId <- joinConnection bob cReq "bob's connInfo" alice' <- liftIO $ getSMPAgentClient cfg subscribeConnection alice' bobId - ("", _, REQ_INV confId "bob's connInfo") <- get alice' - acceptConnection alice' bobId confId "alice's connInfo" + ("", _, CONF confId "bob's connInfo") <- get alice' + allowConnection alice' bobId confId "alice's connInfo" get alice' ##> ("", bobId, CON) get bob ##> ("", aliceId, INFO "alice's connInfo") get bob ##> ("", aliceId, CON) @@ -128,8 +117,8 @@ testAsyncJoiningOfflineBeforeActivation = do (bobId, qInfo) <- createConnection alice SCMInvitation aliceId <- joinConnection bob qInfo "bob's connInfo" disconnectAgentClient bob - ("", _, REQ_INV confId "bob's connInfo") <- get alice - acceptConnection alice bobId confId "alice's connInfo" + ("", _, CONF confId "bob's connInfo") <- get alice + allowConnection alice bobId confId "alice's connInfo" bob' <- liftIO $ getSMPAgentClient cfg {dbFile = testDB2} subscribeConnection bob' aliceId get alice ##> ("", bobId, CON) @@ -152,8 +141,8 @@ testAsyncBothOffline = do disconnectAgentClient bob alice' <- liftIO $ getSMPAgentClient cfg subscribeConnection alice' bobId - ("", _, REQ_INV confId "bob's connInfo") <- get alice' - acceptConnection alice' bobId confId "alice's connInfo" + ("", _, CONF confId "bob's connInfo") <- get alice' + allowConnection alice' bobId confId "alice's connInfo" bob' <- liftIO $ getSMPAgentClient cfg {dbFile = testDB2} subscribeConnection bob' aliceId get alice' ##> ("", bobId, CON) From 57c9ccfc08fa7f9d884e4089397a5132d4736409 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 6 Dec 2021 09:05:13 +0000 Subject: [PATCH 14/20] use base64url encoding for public key in connection requests; only allow accepting invitations that were not accepted (#213) * use base64url encoding for public key in connection requests; only allow accepting invitations that were not accepted * subscribe ContactConnection, fix test to use base64url encoding in key example --- src/Simplex/Messaging/Agent.hs | 2 +- src/Simplex/Messaging/Agent/Protocol.hs | 4 ++-- src/Simplex/Messaging/Agent/Store/SQLite.hs | 19 ++++++++++++++++++- src/Simplex/Messaging/Crypto.hs | 11 ++++++++++- tests/AgentTests.hs | 2 +- 5 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index ef3ed45d2..59714af13 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -340,7 +340,7 @@ subscribeConnection' c connId = Active -> throwError $ CONN SIMPLEX _ -> throwError $ INTERNAL "unexpected queue status" SomeConn _ (RcvConnection _ rq) -> subscribeQueue c rq connId - SomeConn _ (ContactConnection _ _rq) -> pure () + SomeConn _ (ContactConnection _ rq) -> subscribeQueue c rq connId where resumeDelivery :: SndQueue -> m () resumeDelivery SndQueue {server} = do diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index 6c0c322d3..63102111b 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -399,7 +399,7 @@ serializeConnReq' = \case CMContact -> "contact" queryStr = renderSimpleQuery True [("smp", queues), ("e2e", key)] queues = B.intercalate "," . map serializeSMPQueueUri $ L.toList crSmpQueues - key = C.serializePubKey crEncryptKey + key = C.serializePubKeyUri crEncryptKey connReqP' :: forall m. ConnectionModeI m => Parser (ConnectionRequest m) connReqP' = do @@ -414,7 +414,7 @@ connReqP = do crMode <- "/" *> mode <* "#/?" query <- parseSimpleQuery <$> A.takeTill (\c -> c == ' ' || c == '\n') crSmpQueues <- paramP "smp" smpQueues query - crEncryptKey <- paramP "e2e" C.pubKeyP query + crEncryptKey <- paramP "e2e" C.pubKeyUriP query let cReq = ConnReqData {crScheme, crSmpQueues, crEncryptKey} pure $ case crMode of CMInvitation -> ACR SCMInvitation $ CRInvitation cReq diff --git a/src/Simplex/Messaging/Agent/Store/SQLite.hs b/src/Simplex/Messaging/Agent/Store/SQLite.hs index 13d1819d9..57959f9c3 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite.hs @@ -40,7 +40,7 @@ import Data.Maybe (fromMaybe) import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (decodeLatin1) -import Database.SQLite.Simple (FromRow, NamedParam (..), Only (..), SQLData (..), SQLError, field) +import Database.SQLite.Simple (FromRow, NamedParam (..), Only (..), SQLData (..), SQLError, ToRow, field) import qualified Database.SQLite.Simple as DB import Database.SQLite.Simple.FromField import Database.SQLite.Simple.Internal (Field (..)) @@ -382,6 +382,7 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto SELECT contact_conn_id, cr_invitation, recipient_conn_info, own_conn_info, accepted FROM conn_invitations WHERE invitation_id = ? + AND accepted = 0 |] (Only invitationId) where @@ -583,6 +584,22 @@ instance (FromField a, FromField b, FromField c, FromField d, FromField e, fromRow = (,,,,,,,,,,) <$> field <*> field <*> field <*> field <*> field <*> field <*> field <*> field <*> field <*> field <*> field + +instance (FromField a, FromField b, FromField c, FromField d, FromField e, + FromField f, FromField g, FromField h, FromField i, FromField j, + FromField k, FromField l) => + FromRow (a,b,c,d,e,f,g,h,i,j,k,l) where + fromRow = (,,,,,,,,,,,) <$> field <*> field <*> field <*> field <*> field + <*> field <*> field <*> field <*> field <*> field + <*> field <*> field + +instance (ToField a, ToField b, ToField c, ToField d, ToField e, ToField f, + ToField g, ToField h, ToField i, ToField j, ToField k, ToField l) => + ToRow (a,b,c,d,e,f,g,h,i,j,k,l) where + toRow (a,b,c,d,e,f,g,h,i,j,k,l) = + [ toField a, toField b, toField c, toField d, toField e, toField f, + toField g, toField h, toField i, toField j, toField k, toField l + ] {- ORMOLU_ENABLE -} -- * Server upsert helper diff --git a/src/Simplex/Messaging/Crypto.hs b/src/Simplex/Messaging/Crypto.hs index ccd8f5f90..b858d4c0b 100644 --- a/src/Simplex/Messaging/Crypto.hs +++ b/src/Simplex/Messaging/Crypto.hs @@ -64,10 +64,12 @@ module Simplex.Messaging.Crypto -- * Encoding of RSA keys serializePrivKey, serializePubKey, + serializePubKeyUri, encodePubKey, publicKeyHash, privKeyP, pubKeyP, + pubKeyUriP, binaryPubKeyP, -- * SHA256 hash @@ -99,6 +101,7 @@ import qualified Data.Attoparsec.ByteString.Char8 as A import Data.Bifunctor (bimap, first) import qualified Data.ByteArray as BA import Data.ByteString.Base64 (decode, encode) +import qualified Data.ByteString.Base64.URL as U import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.ByteString.Internal (c2w, w2c) @@ -108,7 +111,7 @@ import Data.X509 import Database.SQLite.Simple.FromField (FromField (..)) import Database.SQLite.Simple.ToField (ToField (..)) import Network.Transport.Internal (decodeWord32, encodeWord32) -import Simplex.Messaging.Parsers (base64P, blobFieldParser, parseAll, parseString) +import Simplex.Messaging.Parsers (base64P, base64UriP, blobFieldParser, parseAll, parseString) import Simplex.Messaging.Util (liftEitherError, (<$?>)) -- | A newtype of 'Crypto.PubKey.RSA.PublicKey'. @@ -436,6 +439,9 @@ verify (PublicKey k) (Signature sig) msg = PSS.verify pssParams k msg sig serializePubKey :: PublicKey -> ByteString serializePubKey = ("rsa:" <>) . encode . encodePubKey +serializePubKeyUri :: PublicKey -> ByteString +serializePubKeyUri = ("rsa:" <>) . U.encode . encodePubKey + -- | Base-64 PKCS8 encoding of PSA private key. -- -- Not used as part of SMP protocols. @@ -446,6 +452,9 @@ serializePrivKey = ("rsa:" <>) . encode . encodePrivKey pubKeyP :: Parser PublicKey pubKeyP = decodePubKey <$?> ("rsa:" *> base64P) +pubKeyUriP :: Parser PublicKey +pubKeyUriP = decodePubKey <$?> ("rsa:" *> base64UriP) + -- Binary X509 RSA public key parser. binaryPubKeyP :: Parser PublicKey binaryPubKeyP = decodePubKey <$?> A.takeByteString diff --git a/tests/AgentTests.hs b/tests/AgentTests.hs index 4b8dcd95c..c2bdc5841 100644 --- a/tests/AgentTests.hs +++ b/tests/AgentTests.hs @@ -334,7 +334,7 @@ connect (h1, name1) (h2, name2) = do -- pure (conn1, conn2) samplePublicKey :: ByteString -samplePublicKey = "rsa:MIIBoDANBgkqhkiG9w0BAQEFAAOCAY0AMIIBiAKCAQEAtn1NI2tPoOGSGfad0aUg0tJ0kG2nzrIPGLiz8wb3dQSJC9xkRHyzHhEE8Kmy2cM4q7rNZIlLcm4M7oXOTe7SC4x59bLQG9bteZPKqXu9wk41hNamV25PWQ4zIcIRmZKETVGbwN7jFMpH7wxLdI1zzMArAPKXCDCJ5ctWh4OWDI6OR6AcCtEj+toCI6N6pjxxn5VigJtwiKhxYpoUJSdNM60wVEDCSUrZYBAuDH8pOxPfP+Tm4sokaFDTIG3QJFzOjC+/9nW4MUjAOFll9PCp9kaEFHJ/YmOYKMWNOCCPvLS6lxA83i0UaardkNLNoFS5paWfTlroxRwOC2T6PwO2ywKBgDjtXcSED61zK1seocQMyGRINnlWdhceD669kIHju/f6kAayvYKW3/lbJNXCmyinAccBosO08/0sUxvtuniIo18kfYJE0UmP1ReCjhMP+O+yOmwZJini/QelJk/Pez8IIDDWnY1qYQsN/q7ocjakOYrpGG7mig6JMFpDJtD6istR" +samplePublicKey = "rsa:MIIBoDANBgkqhkiG9w0BAQEFAAOCAY0AMIIBiAKCAQEAtn1NI2tPoOGSGfad0aUg0tJ0kG2nzrIPGLiz8wb3dQSJC9xkRHyzHhEE8Kmy2cM4q7rNZIlLcm4M7oXOTe7SC4x59bLQG9bteZPKqXu9wk41hNamV25PWQ4zIcIRmZKETVGbwN7jFMpH7wxLdI1zzMArAPKXCDCJ5ctWh4OWDI6OR6AcCtEj-toCI6N6pjxxn5VigJtwiKhxYpoUJSdNM60wVEDCSUrZYBAuDH8pOxPfP-Tm4sokaFDTIG3QJFzOjC-_9nW4MUjAOFll9PCp9kaEFHJ_YmOYKMWNOCCPvLS6lxA83i0UaardkNLNoFS5paWfTlroxRwOC2T6PwO2ywKBgDjtXcSED61zK1seocQMyGRINnlWdhceD669kIHju_f6kAayvYKW3_lbJNXCmyinAccBosO08_0sUxvtuniIo18kfYJE0UmP1ReCjhMP-O-yOmwZJini_QelJk_Pez8IIDDWnY1qYQsN_q7ocjakOYrpGG7mig6JMFpDJtD6istR" syntaxTests :: forall c. Transport c => TProxy c -> Spec syntaxTests t = do From d5310c1c6f6b96a54863b677f00e331b4f0a256d Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 6 Dec 2021 12:28:11 +0000 Subject: [PATCH 15/20] SimpleX protocols and network overview (#215) --- protocol/overview.md | 259 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 protocol/overview.md diff --git a/protocol/overview.md b/protocol/overview.md new file mode 100644 index 000000000..40623371e --- /dev/null +++ b/protocol/overview.md @@ -0,0 +1,259 @@ +# SimpleX: messaging and application network + +## Table of contents + +- [SimpleX objectives](#simplex-objectives) +- [SimpleX network](#simplex-network) +- [Threat model](#threat-model) + +## SimpleX objectives + +1. Provide messaging infrastructure for distributed applications. This infrastructure needs to have the following qualities: + + - Security against passive and active (man-in-the-middle) attacks: the parties should have reliable end-to-end encryption and be able to identify and to some extent compensate for the presence of the active attacker who may modify, delete or add messages. + + - Privacy: network server operators should have no ability to read or modify messages without detection, and it should be impossible or hard to infer the contacts the users communicate with. Non-malicious network operators should retain no record of participants communications. + + - Reliability: the messages should be delivered even if some participating network servers or receiving clients fail, with “at least once” delivery guarantee. + + - Integrity: the messages should be delivered without alteration, in the same order, and any unauthorized message change should be detected by the recipient. + + - Asynchronous delivery: it should not be required that both communicating parties (client devices, services or applications) are online for reliable message delivery. + + - Low latency: the delay introduced by the network should not be higher than 100ms-1s in addition to the underlying TCP network latency. + +2. Provide better communication privacy, in particular meta-data privacy (who talks to whom and when), and security, in particular security against active attacks, than the alternative instant messaging solutions. + +3. Balance user experience with privacy requirements, prioritizing experience of mobile device users. + +## SimpleX network + +### Overview + +SimpleX network provides asynchronous messaging infrastructure that aims to deliver on the above objectives and to avoid the disadvantages of the alternative solutions. + +The key differences of SimpleX network are: + +- participants do not need to have globally unique addresses to communicate, instead they use redundant unidirectional (simplex) messaging queues, with a separate set of queues for each contact. + +- connection requests are passed out-of-band, non-optionally protecting key exchange against man-in-the-middle attack. + +- message queues provided by network servers are used by the clients to create more complex communication scenarios, such as duplex one-to-one communication, transmitting files, group communication without central servers and content/communication channels (requiring a separate type of server to host them, which is out of scope of this document). + +- servers do not store any user information (no user profiles, contacts or messages, once they are delivered), and can use in-memory persistence only. + +- users can change server provider(s) without losing their communication contacts, simply by changing the configuration on which servers the new queues are created. + +SimpleX network has design similar to P2P networks, but unlike most P2P networks it consists of clients and servers, without dependency on any centralized component. Servers provide anonymous unidirectional message queues in order to: + +- continuously accept messages for the recipients, even when they are offline. + +- provide recipient anonymity, as the messaging queues do not identify the users, and different addresses of the same queue are used for senders and recipients to provide additional protection against server traffic correlation, in case transport connections are compromised. + +Coincidentally, SimpleX network reminds [Pond messenger](https://github.com/agl/pond) design, the main differences are: + +- Clients can receive messages via multiple servers (in Pond users use one home server and it serves as a user permanent address), + +- Clients do not need to poll servers to receive messages, the messages from subscribed queues are delivered via an open transport connection as they are received by the servers, resulting in low latency of message delivery. + +### SimpleX servers + +Clients communicate with servers (but not with other clients) using SimpleX Messaging Protocol (SMP) running over some transport protocol that provides server authentication, confidentiality with forward secrecy, integrity and transport channel binding (to prevent SMP commands replay). + +Users can use multiple servers. They choose the servers they use to receive the messages through, and they communicate with the servers chosen by the recipients to send messages. + +It is assumed that users have some degree of trust to the servers, for one of the following reasons: + +- They deploy and control the servers themselves from the available open-source code. The advantage of self-deployed servers is that users have full control of server information and activity, but the downside is that it is easier to correlate incoming and outgoing messages by observing server traffic, assuming small number of messages sent via user-hosted servers and irregular communication time. It can be mitigated by: + + - sending noise traffic. + + - future protocol versions can add configurable per queue message delivery delays (compromising on one of the design objectives of low latency). + + - using SMP servers via an onion routing or mix network. + +- They use servers from the trusted commercial providers. The more clients the provider has, the less meta-data about the communication times is leaked to the network observers, and even if transport confidentiality is compromised it is not possible to correlate sent and received traffic without compromising the server, because the clients use different queue IDs to send and receive messages, and the message delivered to the recipient is additionally encrypted by the server. + +- To some extent, users trust their contacts and the servers they chose that the users send messages to. The client applications could further improve user trust to the servers chosen by their contacts by supporting either the list of servers that are “allowed to send to” or “prohibited to send to” (e.g., known trusted or compromised servers, certain IP ranges, server geographical locations etc.). + +If users use the free servers deployed by the volunteers, there is a risk that the server code can maliciously record all queues and messages (even though encrypted) sent via the same transport connection and to gain a partial knowledge of the user’s communications graph and other meta-data. User clients can mitigate it by using some overlay network that protects the privacy of TCP connections, e.g., Tor, and by sending noise traffic between the end users. + +The servers authorize users to access (send/receive/etc.) their message queues via a digital signature of transmissions using a unique key pair different for each queue sender and recipient, so no information that is required for the server to authorize users allows to aggregate user communications across multiple queues. + +### Transport + +SimpleX network does not rely on transport or overlay network for server or client authentication. SMP protocol itself must provide integrity, forward secure confidentiality of communicating parties and mutual authentication even if transport protocol (but not the server) is compromised by the active attacker. + +To protect against MITM attack on the transport, the clients should verify server self-signed certificate during TLS handshake by validating its pre-shared hash of the certificate in the server address (either as part of client configuration or passed in SMP connection request). + +Clients use TLS1.3 protocol to connect to the server, restricting supported cryptographic protocols as defined in SimpleX Messaging Protocol. + +After TCP transport connection is established, and the server is authenticated, the client sends blocks of a fixed size 16Kb, and the server replies with the blocks of the same size. When the actual message is smaller, it should be padded before encryption, and if it is larger, it can be split to chunks by the client application. The first blocks from the client and the server should include protocol version so that the party with the higher version can confirm compatibility and the transport channel identifier as described in SMP protocol. + +Client can terminate the server transport connection without losing any messages – removal of the message from the server and the delivery of the next message happens when the client acknowledges the message reception (normally, after it is persisted in the client’s database). + +Depending on the privacy requirements of the clients, the client can use a separate transport connection per queue, or receive messages from multiple queues via a single transport connection, for efficiency. In our implementations this configuration will be available to the end users. + +### Server implementation + +- does not store user access logs. + +- does not store messages after the client has received them. + +- permanently removes queues after they are deleted by the users + +- only stores messages in transit in server operating memory; as clients are expected to use multiple servers to deliver each message, the message loss in one of the servers is acceptable. + +### SimpleX clients and agents + +Clients are assumed to be running on the trusted devices and can use the locally encrypted storage for messages, contacts, groups and other resources. + +SimpleX clients implementing application-level protocols communicate with SimpleX servers via agents. SimpleX agents can be accessed in one of the following ways: + +- via TCP network using trusted network connection or an overlay transport network. + +- via local port (when the agent runs on the same device as a separate process). + +- via agent library, when the agent logic is included directly into the client application. + +The last option is the most secure, as it reduces the number of attack vectors in comparison with other options. The current implementation of SimpleX Chat uses this approach. + +SimpleX agents provide the following operations: + +- Creating and managing bi-directional (duplex) client connections and delivering messages via redundant groups of SimpleX queues. + +- Exchanging keys and providing end-to-end encryption. The current prototype uses RSA cryptography with ad-hoc hybrid encryption scheme, we are currently switching it to X3DH for key exchange and double ratchet protocol for E2E encryption. + +Communication between the client and the agent and the communication between the agents (via end-to-end encrypted messages delivered via SMP protocol) is described by SimpleX Agent Protocol. + +Bi-directional client connections consist of multiple “send” and multiple “receive” queues (for redundancy) that agents use to send and receive messages; each queue should be used for limited amount of time, the rotation of queues is negotiated using SimpleX Agent Protocol. + +There are two ways that SimpleX network users can establish the connection: + +- Users communicate the initial connection invitation out-of-band, via link or via QR code. This out-of-band communication is assumed secure against active attacks and insecure against passive attacks. Once the connection is established that users are assumed to have confirmed out-of-band, the connection can no longer be compromised even if out-of-band invitation was obtained by the attacker. This out-of-band invitation does not contain any personal information, only the address of the queue and public key for the key exchange for end-to-end encryption. + +- Users can create a “contact queue(s)” on SMP servers that would be used to receive connection requests. The important distinction with commonly used addressing systems is that this queue is only used to pass the connection request and not for subsequent messages. So, while it is possible to spam users who created such contact queues with connection requests, they can be removed without disrupting the communication with the existing contacts. + +The reply queue addresses and the key exchange for the reply queues is negotiated via the direct queue. + +### Message encryption + +Messages are encrypted end-to-end using double ratchet protocol. Once the sending client deletes messages it won’t be able to decrypt them again. While messages are numbered within the queue, this only reveals to the servers how messages have been exchanged in a given queue that has a limited lifetime, so the total number of messages exchanged between contacts is not revealed. + +The next queue to use is negotiated between agents in encrypted messages, and in most cases, it will be on another server (unless the client is configured to use one server, which is not recommended). + +### Client storage + +It is assumed that the client device and the storage is trusted, but clients can implement storage encryption using a user-provided passphrase. + +## Threat model + +### Assumptions + +User protects their information and keys. + +SimpleX Chat software is authentic. + +User device is not compromised, excluding the scenarios below where it is explicitly stated it is compromised. + +Used cryptographic primitives are not compromised. + +### SimpleX Messaging Protocol server + +**can:** + +- learn when a queue recipient or sender is online via transmission times. + +- correlate multiple queues to a single user in case they are accessed via the same transport connection or from the same IP address. + +- correlate multiple queues to a single user based on the time they are accessed. Clients that use message notification servers can mitigate it by only subscribing to the queues after they receive the notifications and disconnecting after some time of inactivity. + +- know how many messages is sent via the queue, clients can mitigate it with noise traffic. + +- add, drop, duplicate or corrupt messages, but it will be detected by the client on SimpleX Agent Protocol level. Using multiple servers to deliver each message mitigates the loss of messages. + +**cannot:** + +- decrypt messages + +- compromise e2e encryption with MITM attack (as server is only used to pass one of two keys in DH exchange, the first key is passed out-of-band) + +### A global passive adversary monitoring all Internet traffic + +**can:** + +- learn who is using SimpleX Chat. + +- learn when messages are sent and received. + +- learn which SimpleX Messaging Protocol servers are used. + +- in case of low traffic on the servers, correlate senders and recipients by the time messages sent and received. + +- observe the approximate size of the files transmitted – to O(log log M) of file size precision. + +**cannot, even in case of compromised transport protocol:** + +- correlate senders and recipients by the content of messages (as different queue IDs are used for senders and recipients, with additional encryption layer in the delivered messages). + +### A local network attacker + +**can:** + +- observe when a user is using SimpleX Chat. + +- block SimpleX Chat traffic. + +- determine which servers the user communicates with. + +- observe the approximate size of files transmissions and receptions. + +**cannot:** + +- see who sends messages to the user and who the user sends the messages to + +### An attacker who obtained user’s decrypted chat database + +**can:** + +- see the history of all messages + +- see shared profiles of contacts and groups + +- receive new user messages, but the user will detect it as the number and the hash of the previous message won’t match. Also, they will be instantly alerted that an unknown device is connected to their message queues, as long as they are online at the same time, as message queues only allow one active subscription. + +- delete user’s message queues, so the contacts won’t be able to send messages. + +- send messages from the user to their contacts, but the recipients will detect it as soon as the user sends the next message, because the previous message hash won’t match (and potentially won’t be able to decrypt them in case they don’t keep the previous ratchet keys). + +**cannot:** + +- impersonate a sender and send messages to the user whose database was stolen without also compromising the server (to place the message in the queue) or the user's device at a subsequent time (to place the message in the database). + +### A user’s contact + +**can:** + +- spam the user with messages. + +- forever retain messages from the user. + +**cannot:** + +- prove to a third-party that a message came from a user (assuming a user’s device is not seized) + +- prove that two contacts they have is the same user. + +- two contacts cannot confirm if they are communicating with the same user. + +### An attacker with Internet access + +**can:** + +- DoS SimpleX messaging servers. + +- spam the user who created a “contact queue” (queue for accepting connection requests) by sending multiple connection requests. + +**cannot:** + +- send messages to the user who they are not connected with. From 218a7756f8e9dd8ac5b4fea34bcab6634e1d7135 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 6 Dec 2021 13:12:10 +0000 Subject: [PATCH 16/20] RJCT / rejectContact to reject contact requests (#214) --- src/Simplex/Messaging/Agent.hs | 20 ++++++++++--- src/Simplex/Messaging/Agent/Protocol.hs | 4 +++ src/Simplex/Messaging/Agent/Store.hs | 1 + src/Simplex/Messaging/Agent/Store/SQLite.hs | 9 ++++++ tests/AgentTests.hs | 33 +++++++++++++++------ 5 files changed, 54 insertions(+), 13 deletions(-) diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 59714af13..9244833c6 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -47,6 +47,7 @@ module Simplex.Messaging.Agent joinConnection, allowConnection, acceptContact, + rejectContact, subscribeConnection, sendMessage, ackMessage, @@ -138,14 +139,18 @@ createConnection c cMode = withAgentEnv c $ newConn c "" cMode joinConnection :: AgentErrorMonad m => AgentClient -> ConnectionRequest c -> ConnInfo -> m ConnId joinConnection c = withAgentEnv c .: joinConn c "" --- | Approve confirmation (ACPT INV command) +-- | Allow connection to continue after CONF notification (LET command) allowConnection :: AgentErrorMonad m => AgentClient -> ConnId -> ConfirmationId -> ConnInfo -> m () allowConnection c = withAgentEnv c .:. allowConnection' c --- | Approve contact (ACPT CON command) +-- | Accept contact after REQ notification (ACPT command) acceptContact :: AgentErrorMonad m => AgentClient -> ConfirmationId -> ConnInfo -> m ConnId acceptContact c = withAgentEnv c .: acceptContact' c "" +-- | Reject contact (RJCT command) +rejectContact :: AgentErrorMonad m => AgentClient -> ConnId -> ConfirmationId -> m () +rejectContact c = withAgentEnv c .: rejectContact' c + -- | Subscribe to receive connection messages (SUB command) subscribeConnection :: AgentErrorMonad m => AgentClient -> ConnId -> m () subscribeConnection c = withAgentEnv c . subscribeConnection' c @@ -237,6 +242,7 @@ withStore action = do SEConnDuplicate -> CONN DUPLICATE SEBadConnType CRcv -> CONN SIMPLEX SEBadConnType CSnd -> CONN SIMPLEX + SEInvitationNotFound -> CMD PROHIBITED e -> INTERNAL $ show e -- | execute any SMP agent command @@ -246,6 +252,7 @@ processCommand c (connId, cmd) = case cmd of JOIN (ACR _ 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) SUB -> subscribeConnection' c connId $> (connId, OK) SEND msgBody -> (connId,) . MID <$> sendMessage' c connId msgBody ACK msgId -> ackMessage' c connId msgId $> (connId, OK) @@ -292,7 +299,7 @@ activateQueueJoining c connId sq verifyKey retryInterval = withStore $ \st -> upgradeSndConnToDuplex st connId rq sendControlMessage c sq . REPLY $ CRInvitation $ ConnReqData CRSSimplex [qUri'] encryptKey --- | Approve confirmation (ACPT INV command) in Reader monad +-- | Approve confirmation (LET command) in Reader monad allowConnection' :: AgentMonad m => AgentClient -> ConnId -> ConfirmationId -> ConnInfo -> m () allowConnection' c connId confId ownConnInfo = do withStore (`getConn` connId) >>= \case @@ -301,7 +308,7 @@ allowConnection' c connId confId ownConnInfo = do processConfirmation c rq senderKey _ -> throwError $ CMD PROHIBITED --- | Accept contact (ACPT CON command) in Reader monad +-- | Accept contact (ACPT command) in Reader monad acceptContact' :: AgentMonad m => AgentClient -> ConnId -> InvitationId -> ConnInfo -> m ConnId acceptContact' c connId invId ownConnInfo = do Invitation {contactConnId, connReq} <- withStore (`getInvitation` invId) @@ -311,6 +318,11 @@ acceptContact' c connId invId ownConnInfo = do joinConn c connId connReq ownConnInfo _ -> throwError $ CMD PROHIBITED +-- | Reject contact (RJCT command) in Reader monad +rejectContact' :: AgentMonad m => AgentClient -> ConnId -> InvitationId -> m () +rejectContact' _ contactConnId invId = + withStore $ \st -> deleteInvitation st contactConnId invId + processConfirmation :: AgentMonad m => AgentClient -> RcvQueue -> SenderPublicKey -> m () processConfirmation c rq sndKey = do withStore $ \st -> setRcvQueueStatus st rq Confirmed diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index 63102111b..97c19b35a 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -190,6 +190,7 @@ data ACommand (p :: AParty) where LET :: ConfirmationId -> ConnInfo -> ACommand Client -- ConnInfo is from client REQ :: InvitationId -> ConnInfo -> ACommand Agent -- ConnInfo is from sender ACPT :: InvitationId -> ConnInfo -> ACommand Client -- ConnInfo is from client + RJCT :: InvitationId -> ACommand Client INFO :: ConnInfo -> ACommand Agent CON :: ACommand Agent -- notification that connection is established SUB :: ACommand Client @@ -661,6 +662,7 @@ commandP = <|> "LET " *> letCmd <|> "REQ " *> reqMsg <|> "ACPT " *> acptCmd + <|> "RJCT " *> rjctCmd <|> "INFO " *> infoCmd <|> "SUB" $> ACmd SClient SUB <|> "END" $> ACmd SAgent END @@ -685,6 +687,7 @@ commandP = letCmd = ACmd SClient <$> (LET <$> A.takeTill (== ' ') <* A.space <*> A.takeByteString) reqMsg = ACmd SAgent <$> (REQ <$> A.takeTill (== ' ') <* A.space <*> A.takeByteString) acptCmd = ACmd SClient <$> (ACPT <$> A.takeTill (== ' ') <* A.space <*> A.takeByteString) + rjctCmd = ACmd SClient . RJCT <$> A.takeByteString infoCmd = ACmd SAgent . INFO <$> A.takeByteString sendCmd = ACmd SClient . SEND <$> A.takeByteString msgIdResp = ACmd SAgent . MID <$> A.decimal @@ -724,6 +727,7 @@ serializeCommand = \case LET confId cInfo -> B.unwords ["LET", confId, serializeBinary cInfo] REQ invId cInfo -> B.unwords ["REQ", invId, serializeBinary cInfo] ACPT invId cInfo -> B.unwords ["ACPT", invId, serializeBinary cInfo] + RJCT invId -> "RJCT " <> invId INFO cInfo -> "INFO " <> serializeBinary cInfo SUB -> "SUB" END -> "END" diff --git a/src/Simplex/Messaging/Agent/Store.hs b/src/Simplex/Messaging/Agent/Store.hs index b387779e6..4c14c5fe5 100644 --- a/src/Simplex/Messaging/Agent/Store.hs +++ b/src/Simplex/Messaging/Agent/Store.hs @@ -55,6 +55,7 @@ class Monad m => MonadAgentStore s m where createInvitation :: s -> TVar ChaChaDRG -> NewInvitation -> m InvitationId getInvitation :: s -> InvitationId -> m Invitation acceptInvitation :: s -> InvitationId -> ConnInfo -> m () + deleteInvitation :: s -> ConnId -> InvitationId -> m () -- Msg management updateRcvIds :: s -> ConnId -> m (InternalId, InternalRcvId, PrevExternalSndId, PrevRcvMsgHash) diff --git a/src/Simplex/Messaging/Agent/Store/SQLite.hs b/src/Simplex/Messaging/Agent/Store/SQLite.hs index 57959f9c3..75158b0c5 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite.hs @@ -405,6 +405,15 @@ instance (MonadUnliftIO m, MonadError StoreError m) => MonadAgentStore SQLiteSto ":invitation_id" := invitationId ] + deleteInvitation :: SQLiteStore -> ConnId -> InvitationId -> m () + deleteInvitation st contactConnId invId = + liftIOEither . withTransaction st $ \db -> + runExceptT $ + ExceptT (getConn_ db contactConnId) >>= \case + SomeConn SCContact _ -> + liftIO $ DB.execute db "DELETE FROM conn_invitations WHERE contact_conn_id = ? AND invitation_id = ?" (contactConnId, invId) + _ -> throwError SEConnNotFound + updateRcvIds :: SQLiteStore -> ConnId -> m (InternalId, InternalRcvId, PrevExternalSndId, PrevRcvMsgHash) updateRcvIds st connId = liftIO . withTransaction st $ \db -> do diff --git a/tests/AgentTests.hs b/tests/AgentTests.hs index c2bdc5841..cf7842d5b 100644 --- a/tests/AgentTests.hs +++ b/tests/AgentTests.hs @@ -20,6 +20,7 @@ import Network.HTTP.Types (urlEncode) import SMPAgentClient import SMPClient (testPort, testPort2, testStoreLogFile, withSmpServer, withSmpServerStoreLogOn) import Simplex.Messaging.Agent.Protocol +import qualified Simplex.Messaging.Agent.Protocol as A import Simplex.Messaging.Protocol (ErrorType (..), MsgBody) import Simplex.Messaging.Transport (ATransport (..), TProxy (..), Transport (..)) import System.Directory (removeFile) @@ -45,11 +46,13 @@ agentTests (ATransport t) = do smpAgentTest2_2_2 $ testDuplexConnection t it "should connect via 2 servers and 2 agents (random IDs)" $ smpAgentTest2_2_2 $ testDuplexConnRandomIds t - describe "Establishing two connections via `contact connection" do - it "should connect via contact contact with one server and 3 agents" $ + describe "Establishing connections via `contact connection`" do + it "should connect via contact connection with one server and 3 agents" $ smpAgentTest3 $ testContactConnection t - it "should connect via contact contact with one server and 2 agents (random IDs)" $ + it "should connect via contact connection with one server and 2 agents (random IDs)" $ smpAgentTest2_2_1 $ testContactConnRandomIds t + it "should support rejecting contact request" $ + smpAgentTest2_2_1 $ testRejectContactRequest t describe "Connection subscriptions" do it "should connect via one server and one agent" $ smpAgentTest3_1_1 $ testSubscription t @@ -175,8 +178,8 @@ testContactConnection _ alice bob tom = do let cReq' = serializeConnReq cReq bob #: ("11", "alice", "JOIN " <> cReq' <> " 14\nbob's connInfo") #> ("11", "alice", OK) - ("", "alice_contact", Right (REQ aConfId "bob's connInfo")) <- (alice <#:) - alice #: ("2", "bob", "ACPT " <> aConfId <> " 16\nalice's connInfo") #> ("2", "bob", OK) + ("", "alice_contact", Right (REQ aInvId "bob's connInfo")) <- (alice <#:) + alice #: ("2", "bob", "ACPT " <> aInvId <> " 16\nalice's connInfo") #> ("2", "bob", OK) ("", "alice", Right (CONF bConfId "alice's connInfo")) <- (bob <#:) bob #: ("12", "alice", "LET " <> bConfId <> " 16\nbob's connInfo 2") #> ("12", "alice", OK) alice <# ("", "bob", INFO "bob's connInfo 2") @@ -188,8 +191,8 @@ testContactConnection _ alice bob tom = do bob #: ("13", "alice", "ACK 1") #> ("13", "alice", OK) tom #: ("21", "alice", "JOIN " <> cReq' <> " 14\ntom's connInfo") #> ("21", "alice", OK) - ("", "alice_contact", Right (REQ aConfId' "tom's connInfo")) <- (alice <#:) - alice #: ("4", "tom", "ACPT " <> aConfId' <> " 16\nalice's connInfo") #> ("4", "tom", OK) + ("", "alice_contact", Right (REQ aInvId' "tom's connInfo")) <- (alice <#:) + alice #: ("4", "tom", "ACPT " <> aInvId' <> " 16\nalice's connInfo") #> ("4", "tom", OK) ("", "alice", Right (CONF tConfId "alice's connInfo")) <- (tom <#:) tom #: ("22", "alice", "LET " <> tConfId <> " 16\ntom's connInfo 2") #> ("22", "alice", OK) alice <# ("", "tom", INFO "tom's connInfo 2") @@ -206,10 +209,10 @@ testContactConnRandomIds _ alice bob = do let cReq' = serializeConnReq cReq ("11", aliceConn, Right OK) <- bob #: ("11", "", "JOIN " <> cReq' <> " 14\nbob's connInfo") - ("", aliceContact', Right (REQ aConfId "bob's connInfo")) <- (alice <#:) + ("", aliceContact', Right (REQ aInvId "bob's connInfo")) <- (alice <#:) aliceContact' `shouldBe` aliceContact - ("2", bobConn, Right OK) <- alice #: ("2", "", "ACPT " <> aConfId <> " 16\nalice's connInfo") + ("2", bobConn, Right OK) <- alice #: ("2", "", "ACPT " <> aInvId <> " 16\nalice's connInfo") ("", aliceConn', Right (CONF bConfId "alice's connInfo")) <- (bob <#:) aliceConn' `shouldBe` aliceConn @@ -223,6 +226,18 @@ testContactConnRandomIds _ alice bob = do bob <#= \case ("", c, Msg "hi") -> c == aliceConn; _ -> False bob #: ("13", aliceConn, "ACK 1") #> ("13", aliceConn, OK) +testRejectContactRequest :: Transport c => TProxy c -> c -> c -> IO () +testRejectContactRequest _ alice bob = do + ("1", "a_contact", Right (INV cReq)) <- alice #: ("1", "a_contact", "NEW CON") + let cReq' = serializeConnReq cReq + bob #: ("11", "alice", "JOIN " <> cReq' <> " 10\nbob's info") #> ("11", "alice", OK) + ("", "a_contact", Right (REQ aInvId "bob's info")) <- (alice <#:) + -- RJCT must use correct contact connection + alice #: ("2a", "bob", "RJCT " <> aInvId) #> ("2a", "bob", ERR $ CONN NOT_FOUND) + alice #: ("2b", "a_contact", "RJCT " <> aInvId) #> ("2b", "a_contact", OK) + alice #: ("3", "bob", "ACPT " <> aInvId <> " 12\nalice's info") #> ("3", "bob", ERR $ A.CMD PROHIBITED) + bob #:# "nothing should be delivered to bob" + testSubscription :: Transport c => TProxy c -> c -> c -> c -> IO () testSubscription _ alice1 alice2 bob = do (alice1, "alice") `connect` (bob, "bob") From 6bdf4f3ff350b48e0597378bdf0ca1159e036426 Mon Sep 17 00:00:00 2001 From: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com> Date: Mon, 6 Dec 2021 18:56:07 +0400 Subject: [PATCH 17/20] change ports for tests (fix for ports now in use on macOS) (#217) --- tests/AgentTests.hs | 2 +- tests/SMPAgentClient.hs | 2 +- tests/SMPClient.hs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/AgentTests.hs b/tests/AgentTests.hs index c2bdc5841..a59888bb6 100644 --- a/tests/AgentTests.hs +++ b/tests/AgentTests.hs @@ -352,7 +352,7 @@ syntaxTests t = 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", "JOIN https://simpex.chat/invitation#/?smp=smp%3A%2F%2Flocalhost%3A5000%2F1234-w%3D%3D%23&e2e=" <> urlEncode True samplePublicKey <> " 14\nbob's connInfo") >#> ("311", "a", "ERR SMP AUTH") + ("311", "a", "JOIN https://simpex.chat/invitation#/?smp=smp%3A%2F%2Flocalhost%3A5001%2F1234-w%3D%3D%23&e2e=" <> urlEncode True samplePublicKey <> " 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") diff --git a/tests/SMPAgentClient.hs b/tests/SMPAgentClient.hs index 99d2336ce..887ac151c 100644 --- a/tests/SMPAgentClient.hs +++ b/tests/SMPAgentClient.hs @@ -157,7 +157,7 @@ cfg :: AgentConfig cfg = defaultAgentConfig { tcpPort = agentTestPort, - smpServers = L.fromList ["localhost:5000#KXNE1m2E1m0lm92WGKet9CL6+lO742Vy5G6nsrkvgs8="], + smpServers = L.fromList ["localhost:5001#KXNE1m2E1m0lm92WGKet9CL6+lO742Vy5G6nsrkvgs8="], tbqSize = 1, dbFile = testDB, smpCfg = diff --git a/tests/SMPClient.hs b/tests/SMPClient.hs index fdd88725b..696de803c 100644 --- a/tests/SMPClient.hs +++ b/tests/SMPClient.hs @@ -32,10 +32,10 @@ testHost :: HostName testHost = "localhost" testPort :: ServiceName -testPort = "5000" +testPort = "5001" testPort2 :: ServiceName -testPort2 = "5001" +testPort2 = "5002" testKeyHashStr :: ByteString testKeyHashStr = "KXNE1m2E1m0lm92WGKet9CL6+lO742Vy5G6nsrkvgs8=" From fe2d6607de44d6be468d3a3a1a8536cf85b4f237 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 7 Dec 2021 19:05:19 +0000 Subject: [PATCH 18/20] delete contact connection with DEL command (#218) --- src/Simplex/Messaging/Agent.hs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 9244833c6..c56063470 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -496,7 +496,8 @@ deleteConnection' c connId = withStore (`getConn` connId) >>= \case SomeConn _ (DuplexConnection _ rq _) -> delete rq SomeConn _ (RcvConnection _ rq) -> delete rq - _ -> withStore (`deleteConn` connId) + SomeConn _ (ContactConnection _ rq) -> delete rq + SomeConn _ (SndConnection _ _) -> withStore (`deleteConn` connId) where delete :: RcvQueue -> m () delete rq = do From ff2b975cd814e08be1d938d6864638007db47b19 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 8 Dec 2021 15:02:28 +0000 Subject: [PATCH 19/20] prepare v0.5.0 - update versions, changelog (#219) * prepare v0.5.0 - update versions, changelog * Update CHANGELOG.md Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com> --- CHANGELOG.md | 10 ++++++++++ package.yaml | 2 +- simplexmq.cabal | 4 ++-- src/Simplex/Messaging/Agent.hs | 4 ++-- src/Simplex/Messaging/Transport.hs | 10 +++++++--- tests/SMPAgentClient.hs | 2 +- 6 files changed, 23 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88c920a23..bbdcff942 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +# 0.5.0 + +- No changes in SMP server implementation - it is backwards compatible with v0.4.1 +- SMP agent changes: + - URI syntax for SMP queues and connection requests. + - long-term connections links ("contacts") in SMP agent protocol. + - agent command changes: + - `REQ` notification and `ACPT` command are used only with long-term connection links. + - `CONF` notification and `LET` commands are used for normal duplex connections. + # 0.4.1 - Include migrations in the package diff --git a/package.yaml b/package.yaml index a32507ff3..828811db9 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplexmq -version: 0.4.1 +version: 0.5.0 synopsis: SimpleXMQ message broker description: | This package includes <./docs/Simplex-Messaging-Server.html server>, diff --git a/simplexmq.cabal b/simplexmq.cabal index 53291b95b..554d17f7b 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -4,10 +4,10 @@ cabal-version: 1.12 -- -- see: https://github.com/sol/hpack -- --- hash: 1e44584019db4d35d25a97c553870b0960fe7a18b5296f0e49b8084c343276ab +-- hash: 3bdb491a0318dc4b53cba4131f192e4c4e17b42e88043495666d1688a1f95443 name: simplexmq -version: 0.4.1 +version: 0.5.0 synopsis: SimpleXMQ message broker description: This package includes <./docs/Simplex-Messaging-Server.html server>, <./docs/Simplex-Messaging-Client.html client> and diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index c56063470..68c8586dd 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -86,7 +86,7 @@ import Simplex.Messaging.Client (SMPServerTransmission) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Protocol (MsgBody, SenderPublicKey) import qualified Simplex.Messaging.Protocol as SMP -import Simplex.Messaging.Transport (ATransport (..), TProxy, Transport (..), runTransportServer) +import Simplex.Messaging.Transport (ATransport (..), TProxy, Transport (..), currentSMPVersionStr, runTransportServer) import Simplex.Messaging.Util (bshow, tryError) import System.Random (randomR) import UnliftIO.Async (Async, async, race_) @@ -110,7 +110,7 @@ runSMPAgentBlocking (ATransport t) started cfg@AgentConfig {tcpPort} = runReader where smpAgent :: forall c m'. (Transport c, MonadUnliftIO m', MonadReader Env m') => TProxy c -> m' () smpAgent _ = runTransportServer started tcpPort $ \(h :: c) -> do - liftIO $ putLn h "Welcome to SMP v0.4.1 agent" + liftIO . putLn h $ "Welcome to SMP agent v" <> currentSMPVersionStr c <- getAgentClient logConnection c True race_ (connectClient h c) (runAgentClient c) diff --git a/src/Simplex/Messaging/Transport.hs b/src/Simplex/Messaging/Transport.hs index f881af3ec..10b470c98 100644 --- a/src/Simplex/Messaging/Transport.hs +++ b/src/Simplex/Messaging/Transport.hs @@ -45,6 +45,7 @@ module Simplex.Messaging.Transport tGetEncrypted, serializeTransportError, transportErrorP, + currentSMPVersionStr, -- * Trim trailing CR trimCR, @@ -63,7 +64,7 @@ import Data.ByteArray (xor) import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.Functor (($>)) -import Data.Maybe(fromMaybe) +import Data.Maybe (fromMaybe) import Data.Set (Set) import qualified Data.Set as S import Data.String @@ -221,7 +222,10 @@ major :: SMPVersion -> (Int, Int) major (SMPVersion a b _ _) = (a, b) currentSMPVersion :: SMPVersion -currentSMPVersion = "0.4.1.0" +currentSMPVersion = "0.5.0.0" + +currentSMPVersionStr :: ByteString +currentSMPVersionStr = serializeSMPVersion currentSMPVersion serializeSMPVersion :: SMPVersion -> ByteString serializeSMPVersion (SMPVersion a b c d) = B.intercalate "." [bshow a, bshow b, bshow c, bshow d] @@ -372,7 +376,7 @@ serverHandshake c srvBlockSize (k, pk) = do liftError (const $ TEHandshake DECRYPT) (C.decryptOAEP pk encKeys) >>= liftEither . parseClientHandshake sendWelcome_6 :: THandle c -> ExceptT TransportError IO () - sendWelcome_6 th = ExceptT . tPutEncrypted th $ serializeSMPVersion currentSMPVersion <> " " + sendWelcome_6 th = ExceptT . tPutEncrypted th $ currentSMPVersionStr <> " " -- | Client SMP encrypted transport handshake. -- diff --git a/tests/SMPAgentClient.hs b/tests/SMPAgentClient.hs index 99d2336ce..3bd6315ac 100644 --- a/tests/SMPAgentClient.hs +++ b/tests/SMPAgentClient.hs @@ -189,7 +189,7 @@ testSMPAgentClientOn :: (Transport c, MonadUnliftIO m) => ServiceName -> (c -> m testSMPAgentClientOn port' client = do runTransportClient agentTestHost port' $ \h -> do line <- liftIO $ getLn h - if line == "Welcome to SMP v0.4.1 agent" + if line == "Welcome to SMP agent v" <> currentSMPVersionStr then client h else error $ "wrong welcome message: " <> B.unpack line From f15067cf6891bda3216c6cf6d2e3ecdba9b7269e Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 8 Dec 2021 15:11:27 +0000 Subject: [PATCH 20/20] 0.5.0