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 1/2] 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 2/2] 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"