Compare commits

...
206 Commits
Author SHA1 Message Date
Efim Poberezkin a406159ed4 1.0.3 2022-02-26 18:11:07 +04:00
Efim Poberezkin 14780a47d6 prepare v1.0.3 (#324) 2022-02-26 18:07:11 +04:00
Evgeny Poberezkin 4a66f68c55 reduce server message queue quota to 128 messages (#323) 2022-02-26 17:49:21 +04:00
Evgeny Poberezkin d1e6147adf encode all nullary constructors as objects for sumTypeJSON options 2022-02-25 18:50:04 +00:00
Evgeny Poberezkin 7d1fdadef0 reduce connection handshake expiration to 2 days 2022-02-25 10:57:23 +00:00
Evgeny Poberezkin 09e2e75c42 increase queue sizes (#322)
* increase queue sizes

* remove TODO
2022-02-25 09:02:52 +00:00
Evgeny Poberezkin dff5cad1be make CPP condition depend on HOST OS (#321) 2022-02-18 08:12:08 +00:00
Evgeny Poberezkin c1f5f9d846 flip condition for JSON encoding to make android (tagged objects) default (#319) 2022-02-16 22:38:15 +00:00
Evgeny Poberezkin 9c4778b129 update condition in cabal 2022-02-16 17:28:14 +00:00
Evgeny Poberezkin 137afb68fe conditional JSON options for iOS/Android (#318)
* conditional JSON options for iOS/Android

* update cabal file
2022-02-16 08:58:35 +00:00
Efim Poberezkin 229e2607d7 remove redundant asynchronous connection code (#317) 2022-02-14 21:00:14 +04:00
Efim PoberezkinandEvgeny Poberezkin c380c79560 make new smp client attempt to reconnect on network error (#315)
* make new smp client attempt to reconnect on network error

* reconnect async wip

* remove comments

* trigger tryReconnectClient

* add subscription before trying to take smp client

* move atomically modify asyncClients

* remove logs

* corrections

* don't put error into new smp client TMVar

* UnliftIO async

* re-work

* test for connection to the server that was initially down

* fix tests

* additionally test that bob receives the message after re-connect

* make test independent of event order

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-02-10 14:48:11 +00:00
Efim Poberezkin c9994c3a2c add "yes to migrations" option (#316) 2022-02-07 11:51:39 +04:00
Evgeny Poberezkin 137ff7043d use aeson fork with the option to encode/decode nullary constructors as empty objects (#312)
* use aeson fork with the option to encode/decode nullary constructors as empty objects

* update dependencies
2022-01-29 19:36:02 +00:00
Evgeny Poberezkin 6fe3bfa980 JSON encoding of types used in simplex-chat (#311)
* JSON encoding of types used in simplex-chat

* add field names for JSON encodings, encode all error sum-types as objects (to allow extension)
2022-01-26 20:18:41 +00:00
Evgeny Poberezkin 2b857876b4 update release template 2022-01-21 18:13:02 +00:00
Evgeny Poberezkin b777a4fd93 split the server dependencies from the agent (#310)
* split the server dependencies from the agent

* remove redundant imports

* fix Main.hs
2022-01-21 17:33:47 +00:00
Efim Poberezkin e15a25d92e update Linode stackscript, amendments 2022-01-21 14:21:07 +04:00
Efim Poberezkin 1cd68f4159 update Linode stackscript (#309) 2022-01-21 12:36:42 +04:00
Efim Poberezkin 495439adf5 1.0.2 2022-01-21 11:40:49 +04:00
Efim Poberezkin 98fac579c0 switch tls from fork to original repo (#307) 2022-01-21 11:26:16 +04:00
Evgeny Poberezkin 670b3b7974 coalesce requests to connect to SMP servers, to have 1 connection per server (#305)
* coalesce requests to connect to SMP servers

* fix possible race condition when creating new SMP client

* one more race condition

* close pending SMP clients
2022-01-20 18:33:02 +00:00
Efim PoberezkinandEvgeny Poberezkin 305ae94cce move TLS handshake to a separate thread per-connection (#306)
* move handshake to a separate thread

* 1.0.2-rc04

* refactor TLS thread

* hide EOF exception

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-01-20 18:32:09 +00:00
Evgeny Poberezkin a9a6917056 replace file-embed with QQ (#301) 2022-01-20 18:31:09 +00:00
Efim Poberezkin 502ee39eb3 publish release as prerelease if tag contains 'rc'; update ghc version in build (#303) 2022-01-20 11:38:02 +04:00
Efim Poberezkin ac899a67c4 support TLS 1.3 (#300) 2022-01-18 18:59:02 +04:00
Efim Poberezkin 6e7089284e cease attempts to send HELLO after timeout (#298) 2022-01-18 13:50:43 +04:00
Efim Poberezkin 40efdf97de bootstrap script to download latest SMP server release for DigitalOcean droplet (#295) 2022-01-17 12:32:10 +04:00
Efim Poberezkin 7e0bcc7aa0 v1.0.1 (#294) 2022-01-15 18:39:57 +04:00
Efim Poberezkin 56fea79097 add not up to date warning for DO (#293) 2022-01-15 12:51:23 +04:00
Evgeny Poberezkin 26a01dfc40 explicitely set line buffering in stdout/stderr to log each lines when output is redirected to files (#290) 2022-01-14 08:34:34 +00:00
Efim Poberezkin 9c3962bbe3 update readme on initialization, fix servers list (#288) 2022-01-13 10:39:02 +04:00
Evgeny Poberezkin 45e264c398 v1 2022-01-12 16:20:55 +00:00
Evgeny Poberezkin 41047b5db8 Merge pull request #269 from simplex-chat/ep/v1-rc
v1 release
2022-01-12 16:14:07 +00:00
Evgeny Poberezkin 15f01af6cf update changelog 2022-01-12 11:13:14 +00:00
Evgeny Poberezkin bfa4911217 improve message error handling (#286)
* message envelope sizes and protocol doc corrections

* change error handling in message delivery loop
2022-01-12 10:36:22 +00:00
Efim Poberezkin d1eba1ef2f update README instructions on server fingerprint and address (#285) 2022-01-12 11:28:16 +04:00
Evgeny Poberezkin 5f20f0124a correction to E2E encrypted (double ratchet) envelope size 2022-01-11 22:08:37 +00:00
Evgeny Poberezkin 38f4cbe74f Merge branch 'master' into ep/v1-rc 2022-01-11 21:20:27 +00:00
Evgeny Poberezkin c071adb041 update changelog 2022-01-11 21:19:12 +00:00
Evgeny Poberezkin 657c9b1b37 reduce SMP envelope sizes (#284) 2022-01-11 21:01:21 +00:00
Evgeny Poberezkin 4c0414d2d6 update readme 2022-01-11 20:51:39 +00:00
Efim Poberezkin 2f342d3914 clarify wording in Linode script comment on server address 2022-01-11 21:58:40 +04:00
Efim Poberezkin 70823b899f remove Linode referral link (#283) 2022-01-11 21:11:27 +04:00
Efim Poberezkin b6aa0c886a link to smp-server by simplexchat StackScript (#282) 2022-01-11 20:57:47 +04:00
Efim Poberezkin 3909c7b10e remove Subject Alternative Names from server certificate (#281) 2022-01-11 20:48:27 +04:00
Efim Poberezkin 5306489d72 initialize server with IP and FQDN; specify FQDN as address if provided (#279) 2022-01-11 20:24:39 +04:00
Evgeny PoberezkinandEfim Poberezkin f62cc8bda5 Update CHANGELOG.md
Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com>
2022-01-11 16:11:04 +00:00
Evgeny Poberezkin d8a61de290 Merge branch 'master' into ep/v1-rc 2022-01-11 16:02:10 +00:00
Evgeny Poberezkin 083d39be22 store missing port as empty string instead of NULL (#280) 2022-01-11 16:01:09 +00:00
Efim Poberezkin d48da40f7e add IP and FQDN to online certificate (#278) 2022-01-11 16:35:06 +04:00
Evgeny Poberezkin 1cf68ae448 note in readme 2022-01-11 07:49:49 +00:00
Evgeny Poberezkin c81e667e01 update version and changelog 2022-01-11 07:41:41 +00:00
Evgeny Poberezkin c95854ee3f Merge branch 'master' into ep/v1-rc 2022-01-11 07:28:26 +00:00
Evgeny Poberezkin b5cb5618c1 JSON instances for ConnectionRequestUri (#276) 2022-01-11 07:21:53 +00:00
Efim Poberezkin 14afa24a0a disable websockets by default (#277) 2022-01-11 11:21:02 +04:00
Efim Poberezkin 0bfac998a5 update Linode and DigitalOcean server recipes (#274) 2022-01-11 10:41:02 +04:00
Evgeny Poberezkin 0775bceb5e remove unnecessary separation of UnliftIO instance for ExceptT (#275) 2022-01-10 19:06:04 +00:00
Efim Poberezkin db5ae65f3c Merge branch 'stable' 2022-01-10 22:17:33 +04:00
Efim Poberezkin 92b3137b79 non interactive apt commands in server recipes (#272) 2022-01-10 22:08:38 +04:00
Evgeny Poberezkin 77b1d05da2 preparing v1 release 2022-01-10 14:04:41 +00:00
Evgeny Poberezkin 2a89394174 integrate double ratchet into agent (#268)
* separate skipped messages from ratchet

* return diff for skipped messages instead of the whole state (tests fail)

* fix ratchet tests

* JSON encoding/decoding for ratchet state

* schema for ratchets

* split MonadUnliftIO instance for ExceptT to a separate file

* update StrEncoding instance for Str

* ratchet store methods

* updateRatchet store method

* move E2E ratchet params to Ratchet module

* x3dh key agreement for double ratchet

* test/fix x3dh, use x3dh for ratchets initialization

* store/get x3dh keys, save ratchet of fixed X448 type

* double-ratchet encryption integration (tests fail)

* fix double ratchet

* fix padding and message length

* remove unused code for "activations"

* remove comment

* add version checks for forward/backward compatibility

* split loading ratchet and skipped message keys

* remove unused encoding instances for Algorithm types

* update ratchet initialization params
2022-01-10 12:01:54 +00:00
Efim Poberezkin 4d68042884 change initial migration date (migrations merged) (#267) 2022-01-07 17:03:00 +04:00
Efim Poberezkin 5e57c5dfd7 rename remaining occurrences of connection alias into connection id (#265) 2022-01-07 15:37:40 +04:00
Efim Poberezkin d87ff42cf5 start with internal message id 1; fix id ordering in tests (#264) 2022-01-07 14:52:37 +04:00
Evgeny Poberezkin c4f7020aae Merge pull request #263 from simplex-chat/ep/delete-control-messages
delete control messages after ACK to server
2022-01-07 08:54:20 +00:00
Evgeny Poberezkin 624360bf39 delete control messages after ACK to server 2022-01-07 08:47:35 +00:00
Evgeny Poberezkin ee5f144ea5 Merge pull request #259 from simplex-chat/ep/message-delivery
concurrent message delivery with a separate thread/queue per connection
2022-01-06 16:47:07 +00:00
Evgeny Poberezkin 7ca0ead6a2 Merge branch 'master' into ep/message-delivery 2022-01-06 16:42:16 +00:00
Evgeny Poberezkin b415537ba5 simplify message delivery 2022-01-06 16:41:39 +00:00
Evgeny Poberezkin a5a00f3d02 Merge branch 'stable' into ep/message-delivery 2022-01-06 16:38:22 +00:00
Evgeny Poberezkin dd4ccce1ba 0.5.2 2022-01-06 13:52:54 +00:00
Evgeny Poberezkin 584f230c4d update versions for 0.5.2, changelog (#261) 2022-01-06 13:50:50 +00:00
Evgeny Poberezkin 10c62e7fa2 fix message delivery when quota exceeded - making it concurrent per queue, not per server (#260)
* test: HELLO blocking message delivery on the same server

* test: quota exceeded in one queue should not block delivery in other queues

* fix test to work in stable branch

* simplify pending message delivery (#202)

* simplify pending message delivery (WIP)

* refactor

* fix concurrent message delivery

* remove type synonym
2022-01-06 08:56:19 +00:00
Evgeny Poberezkin 02c023f939 remove message statuses and unused timestamps (#257) 2022-01-06 08:41:39 +00:00
Evgeny Poberezkin 6f1d9db8ec test: quota exceeded in one queue should not block delivery in other queues 2022-01-05 21:48:40 +00:00
Evgeny Poberezkin 3c923a3dc0 test: HELLO blocking message delivery on the same server 2022-01-05 21:12:31 +00:00
Evgeny Poberezkin ec0881f50f remove message statuses and unused timestamps 2022-01-05 19:57:04 +00:00
Evgeny PoberezkinandEfim Poberezkin 488398df9f change message envelopes and encoding, unify message delivery (#252)
* types and encodings for double ratchet integration

* upgrade stack resolver

* type classes for version agreement, encode/decode connection request links and E2E params with versioning

* encode/decode client parameters (version and DH key) in SMP queue URI using query string parameters

* restore support of the current SMP queue URI format

* update AMessage to only send queues in REPLY message (not the full connection request)

* new agent message evnvelopes (tests fail)

* new message envelopes - tests pass

* store fully encrypted messages before sending

* unify message delivery via DB queue (excluding confirmation and invitation)

* remove activateSecuredQueue

* linter hints

* remove comment

* export order

* save rachet-encrypted message, not per-queue encrypted

* delete message after it is accepted by the server, reduce message delivery interval for the tests

Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com>
2022-01-05 19:52:37 +00:00
Efim Poberezkin e452c6ebff delete received messages after sending ACK to server (#256) 2022-01-05 16:52:28 +04:00
Efim Poberezkin 19edf09ae2 add deleteMsg to agent store interface (#255) 2022-01-05 16:11:52 +04:00
Efim Poberezkin 4727e568d5 fix x509 call to be compatible with openssl version 1.1.1 (#253) 2022-01-03 20:59:53 +04:00
Evgeny Poberezkin 4a73a7ecd4 remove public header from all messages, fix envelope sizes (#251)
* fix envelope sizes

* only send sender DH pub key for per-queue E2E with confirmation message
2022-01-03 14:33:30 +00:00
Evgeny PoberezkinandEfim Poberezkin 83d085cadc amend SMP protocol doc (#232)
* amend SMP protocol doc

* Update protocol/simplex-messaging.md

Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com>

* Update protocol/simplex-messaging.md

Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com>

* Update protocol/simplex-messaging.md

* Update protocol/simplex-messaging.md

Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com>
2022-01-02 22:48:01 +00:00
Evgeny Poberezkin f3523bbba9 make KeyHash non-optional, verify KeyHash in SMP handshake, use StrEncoding class (#250)
* make KeyHash non-optional, StrEncoding class

* change server URI format in agent config, refactor with StrEncoding

* refactor Crypto using checkAlgorithm

* refactor parsing connection requests

* prepare to validate CA fingerprint sent in client handshake

* KeyHash check in handshake

* rename type to CliCommand

* server validates keyhash sent by the client

* validate -a option when parsing

* more of StrEncoding
2022-01-02 22:24:43 +00:00
Efim Poberezkin 33bb38299b refactor server Main.hs (#248) 2022-01-02 21:49:40 +04:00
Evgeny Poberezkin f314ff1bb6 add links to overview (#249) 2022-01-01 14:22:43 +00:00
Evgeny PoberezkinandEfim Poberezkin 1737686c1e update overview (#233)
* update overview

* network/framework -> platform

* fix links

* corrections

* corrections

* correction

* correction

* corrections

* update acknowledgements

* remove old overview.md

* update cyptographic primitives, remove acknoledgement

Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com>
2022-01-01 13:59:03 +00:00
Evgeny Poberezkin 5e29e3698e binary SMP protocol encoding, split Command type to two types (#245)
* binary SMP protocol encoding (server tests fail)

* use 1 byte for bytestring length when encoding/decoding

* Encoding class, binary tags

* update server tests

* negotiate SMP version in client/server handshake

* add version columns to queues and connections

* split parsing SMP client commands and server responses to different functions

* check uniqueness of protocol tags

* split client commands and server responses/messages to separate types

* update types in SMP client

* remove pattern synonyms for SMP errors

* simplify getHandshake

* update SMP protocol encoding in protocol spec

* encode time as a number of seconds (64-bit integer) since epoch
2022-01-01 13:10:19 +00:00
Evgeny PoberezkinandEfim Poberezkin 5e3f66a4cb update DB fields presentations: port, QueueStatus, RcvMsgStatus, SndMsgStatus (#244)
* make port nullable

* make conversions of enum types to/from text explicit; remove unused statuses/fields

* Update src/Simplex/Messaging/Agent/Store.hs

* rename RcvMsgStatus constructors

Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com>
2021-12-30 09:33:47 +00:00
Evgeny Poberezkin c8178e7f1f improve crypto key type classes (#246)
* improve crypto key type classes

* add inline pragmas
2021-12-30 08:51:20 +00:00
Evgeny Poberezkin c66f9efa55 merge migrations, prepare schema for multiple queues per connection (#243)
* merge migrations into initial

* update connection/queue tables in schema, simplify creating connections

* refactor create connection

* remove queue fields from connections table

* update server constraints
2021-12-29 17:55:27 +00:00
Evgeny Poberezkin 73cad5a6c4 simple per-queue e2e encryption with NaCl crypto_box (#242)
* simple per-queue e2e encryption with NaCl crypto_box

* add e2e keys and DH secrets to schema

* agree and save shared DH secret per queue (not used yet)

* protocol changes for uniform padding and message part lengths

* correct message structure diagrams

* make per-queue E2E encryption non-optional

* refactor crypto keys

* use NaCl crypto_box for per-queue E2E encryption, remove RSA keys from queues

* remove RSA support

* merge migration with E2E DH keys

* clean up

* remove unused methods

* parsing/serializing agent messages

* remove sender timestamp from DB and code

* clean up

* slean up

* s/SMPConfMsg/SMPConfirmation/

* serializeAgentMessage = serializeClientMessage . agentToClientMsg

* simplify error handling

* update protocol docs
2021-12-29 14:27:10 +00:00
Evgeny PoberezkinandEfim Poberezkin 51a9750891 double ratchet algorithm implementation (#236)
* started double ratchet implementation

* initialize ratchets

* started ratchet encryption

* ratchet encryption

* simplify / narrow down Ratchet type

* double ratchet decryption "framework"

* advance receive ratched on skipped messages

* more ratchet decryption

* double ratchet encrypt/decrypt (TODO tests)

* double ratchet tests

* double ratchet tests

* use ratchet AD in header encryption, use header and ratchet AD as AD in message encryption

* change ratchet message error, remove Show instances

* Update tests/AgentTests/DoubleRatchetTests.hs

Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com>

* Update tests/AgentTests/DoubleRatchetTests.hs

Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com>

* Update tests/AgentTests/DoubleRatchetTests.hs

Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com>

* Update tests/AgentTests/DoubleRatchetTests.hs

Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com>

* Update tests/AgentTests/DoubleRatchetTests.hs

Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com>

* Update src/Simplex/Messaging/Crypto/Ratchet.hs

Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com>

* test in the same ratchet step

* merge tests

* Update src/Simplex/Messaging/Crypto/Ratchet.hs

* Update src/Simplex/Messaging/Crypto/Ratchet.hs

* remove HMAC comment

Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com>
2021-12-25 17:13:53 +00:00
Evgeny Poberezkin 7bf97fac18 add sessionId and blockSize to the client record used internally (#241) 2021-12-24 19:31:39 +00:00
Efim Poberezkin 14954df6b9 print version on server initialization & start-up (#240) 2021-12-24 15:44:40 +04:00
Efim Poberezkin 388e4d2bcc update server recipes - Linode & DigitalOcean (#239) 2021-12-24 15:34:09 +04:00
Efim Poberezkin 0c866105d2 chain of two certificates - offline (identity) and online; switch certificates to v3 (#238) 2021-12-23 21:20:41 +04:00
Efim Poberezkin 96cf4d288c base64url encode fingerprint in agent test (#237) 2021-12-23 13:04:56 +04:00
Efim PoberezkinandEvgeny Poberezkin e2cd370513 certificate validation on client side; check stored fingerprint on server start-up; non-optional fingerprint parsing (#234, closes #155)
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2021-12-22 23:24:58 +04:00
Evgeny Poberezkin f9f1b8f355 new overview (#235) 2021-12-21 07:46:22 +00:00
Evgeny Poberezkin f154f7ee3a overview doc updates (#216)
* overview changes

* correction

* protocol docs changes

* include reviewer comments in the doc

* encryption scheme for the recipient traffic

* TLS cipher suite
2021-12-20 12:07:31 +00:00
Evgeny Poberezkin 1873e82804 add CODEOWNERS (#231) 2021-12-20 07:08:52 +00:00
Evgeny PoberezkinandEfim Poberezkin 400e057dab use tls-unique as session ID, switch to TLS 1.2 in tls package fork (#230)
* use tls-unique as session ID, switch to TLS 1.2 in tls package fork

* Update src/Simplex/Messaging/Transport.hs

* Update src/Simplex/Messaging/Transport/WebSockets.hs

Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com>

Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com>
2021-12-19 15:10:37 +00:00
Efim PoberezkinandEvgeny Poberezkin 129246c9e6 unencrypted SMP transport over TLS; simplified handshake (#229)
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2021-12-19 13:04:44 +04:00
Evgeny Poberezkin 8ca2d64bc9 Update FUNDING.yml 2021-12-18 16:28:06 +00:00
Evgeny Poberezkin e0acb42a28 Merge branch 'stable' 2021-12-17 15:44:58 +00:00
Evgeny Poberezkin bd2e251cae server update script 2021-12-17 13:23:37 +00:00
Evgeny Poberezkin 6b5cc3d2d5 0.5.1 2021-12-17 12:33:01 +00:00
Evgeny PoberezkinandEfim Poberezkin 5f7fe8b0dc remove client from servers subscribers map after client disconnection (#228)
Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com>
2021-12-17 16:28:48 +04:00
Evgeny Poberezkin 1df146c702 make WebSockets use TLS Context (#227) 2021-12-16 07:30:16 +00:00
Evgeny Poberezkin 323fb1f03c remove SMP server signing responses (#226)
* remove SMP server signing responses

* keep only one session ID
2021-12-16 07:15:45 +00:00
Efim Poberezkin de01692ffd enable WebSockets over TLS (#225) 2021-12-15 22:58:47 +04:00
Efim Poberezkin bcf5e25cab create TLS ServerParams once per server run; remove tlsServerParams from agent env (fixes functional agent client for chat) (#223) 2021-12-15 19:03:34 +04:00
Evgeny Poberezkin 5aa0e97cd9 sign server responses 2021-12-15 11:52:09 +00:00
Evgeny Poberezkin cf3d0dfdc3 Transaction fields for size, session IDs, refactor (#222)
* add SMP session IDs/tls-unique to transmission

* refactor SMP transmissions: precise transmission types in server & client

* use correct session IDs

* remove TSession
2021-12-15 08:06:34 +00:00
Evgeny PoberezkinandEfim Poberezkin 83d352cfbe TLS 1.3 transport (#203)
* TLS as Transport class instance with pre-defined server certificate/key

* refactor error logging

* remove Ed25519

* refactor TLS.cGet

* TLS over TCP for Transport

* Plain -> TLS

* comment

* getLn, change supported cipher

* use non fixed certificates

* comment

* check options earlier

* wording

* headers

* Update apps/smp-server/Main.hs

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>

* Update apps/smp-server/Main.hs

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>

* localhost -> server

* Update apps/smp-server/Main.hs

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>

* remove comment

* agent key and cert fixtures WIP

* certificate and key in correct order

* exitFailure

* refactor loadServerCertificate

* remove liftIO

Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com>
2021-12-15 07:48:57 +00:00
Evgeny Poberezkin 7dba734ab8 Merge pull request #221 from simplex-chat/ep/smp-sign-encrypt
SMP protocol changes: server signature keys (no signing/verification yet), recipient message bodies encryption with crypto_box
2021-12-13 12:49:41 +00:00
Evgeny PoberezkinandEfim Poberezkin a3101edbb9 Update src/Simplex/Messaging/Agent/Client.hs
Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com>
2021-12-13 12:04:52 +00:00
Evgeny Poberezkin 95fbd70346 encrypt recipient message bodies with crypto_box 2021-12-13 10:56:26 +00:00
Evgeny Poberezkin acf5c15a05 increase SMP queue ID and message ID size to 24 bytes 2021-12-12 23:17:21 +00:00
Evgeny Poberezkin 560d9ceb71 add NaCl crypto_box scheme 2021-12-12 22:59:11 +00:00
Evgeny Poberezkin bfa05c9432 all tests pass! 2021-12-12 21:17:25 +00:00
Evgeny Poberezkin c45454d9e5 update server store log to save/restore additional keys, use Ed keys in tests instead of RSA 2021-12-12 20:51:34 +00:00
Evgeny Poberezkin f6991539a2 add server signature keys and DH keys for server->recipient msg bodis to SMP commands (server store log and agent tests are skipped) 2021-12-12 12:22:44 +00:00
Evgeny Poberezkin 5f7366b7bd Merge pull request #198 from simplex-chat/v5 2021-12-10 12:29:50 +00:00
Evgeny Poberezkin f640e71f82 remove BlockArguments extension (#220) 2021-12-10 12:21:54 +00:00
Evgeny Poberezkin 52eef2d7c7 Merge branch 'master' into v5 2021-12-10 11:52:08 +00:00
Evgeny Poberezkin f15067cf68 0.5.0 2021-12-08 15:11:27 +00:00
Evgeny PoberezkinandEfim Poberezkin ff2b975cd8 prepare v0.5.0 - update versions, changelog (#219)
* prepare v0.5.0 - update versions, changelog

* Update CHANGELOG.md

Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com>
2021-12-08 15:02:28 +00:00
Evgeny Poberezkin fe2d6607de delete contact connection with DEL command (#218) 2021-12-07 19:05:19 +00:00
Efim Poberezkin 6bdf4f3ff3 change ports for tests (fix for ports now in use on macOS) (#217) 2021-12-06 18:56:07 +04:00
Evgeny Poberezkin 218a7756f8 RJCT / rejectContact to reject contact requests (#214) 2021-12-06 13:12:10 +00:00
Evgeny Poberezkin d5310c1c6f SimpleX protocols and network overview (#215) 2021-12-06 12:28:11 +00:00
Evgeny Poberezkin 82ae5c623f Merge branch 'master' into v5 2021-12-06 09:22:45 +00:00
Evgeny Poberezkin 57c9ccfc08 use base64url encoding for public key in connection requests; only allow accepting invitations that were not accepted (#213)
* use base64url encoding for public key in connection requests; only allow accepting invitations that were not accepted

* subscribe ContactConnection, fix test to use base64url encoding in key example
2021-12-06 09:05:13 +00:00
Evgeny Poberezkin 58758e0572 Merge branch 'master' into v5 2021-12-04 21:08:38 +00:00
Evgeny Poberezkin 3e226fc3f2 add CONF/LET for invitations, use REQ/ACPT only with contact connections (#212) 2021-12-04 21:08:02 +00:00
Evgeny Poberezkin b116408ec3 Merge branch 'master' into v5 2021-12-04 18:55:23 +00:00
Evgeny Poberezkin 04c65d98da 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
2021-12-04 18:42:08 +00:00
Evgeny Poberezkin 316dc7b320 merge protocol changes from v5 (#211) 2021-12-02 18:42:13 +00:00
Evgeny Poberezkin 6126e72d4f Merge branch 'master' into v5 2021-12-02 18:06:42 +00:00
Evgeny Poberezkin 606bbf5062 URI syntax for SMP queues and connection requests (#210)
* URI syntax for SMP queues and connection requests

* fix connection request parsing/serializing

* add ConnectionRequest sqlite instances
2021-12-02 17:43:05 +00:00
Evgeny Poberezkin bebd1e5fb9 simplex URI request 2021-11-29 23:00:26 +00:00
Evgeny PoberezkinandEfim Poberezkin db509d0311 update protocol docs (#209)
* update protocol docs

* more protocol corrections

* update connection request syntax

* Update protocol/agent-protocol.md

Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com>

* Update protocol/agent-protocol.md

Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com>

* Update protocol/agent-protocol.md

Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com>

* Update protocol/agent-protocol.md

Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com>

* Update protocol/simplex-messaging.md

Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com>

Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com>
2021-11-28 21:01:34 +00:00
Evgeny Poberezkin e1002d5ac0 GADTs for cryptographic keys (#208)
* GADTs for cryptographic keys

* update tests (signature timing tests still fail)

* fix signature verification timing tests

* configurable algorithm to sign commands to SMP queues (Ed448 by default)

* add dummy Ed keys, add timing tests for Ed keys

* re-enable Connection subscriptions tests
2021-11-28 11:44:22 +00:00
Evgeny Poberezkin 99b3749890 simplify RSA private key types (#207)
* simplify RSA private key types

* remove updateSignKey method
2021-11-28 07:08:47 +00:00
Efim Poberezkin ab875198ed SMP protocol typo 2021-11-27 02:31:15 +10:00
Evgeny Poberezkin f3d79ebcda update SMP protocol to include server signature and encryption keys 2021-11-25 17:24:00 +00:00
Evgeny Poberezkin 01e8c232f0 change syntax for queue URI and connection request (#205)
* change syntax for queue URI and connection request

* separate queue URI into separate section

* correction
2021-11-22 19:08:19 +00:00
Evgeny PoberezkinandEfim Poberezkin eb7fcae31b update transport protocol to use TLS (#204)
* update transport protocol to use TLS

* typos

* s/serverKeyHash/serverIdentity/

Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com>

* Update protocol/simplex-messaging.md

Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com>

* corrections

Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com>
2021-11-22 08:52:39 +00:00
Evgeny Poberezkin eb941f7435 simplify pending message delivery (#202)
* simplify pending message delivery (WIP)

* refactor
2021-11-19 16:26:51 +00:00
Evgeny PoberezkinandEfim Poberezkin 227d83d0e7 SMP commands for notifications (NKEY/NID and NSUB/NMSG) with separate queue ID and key (#199)
* SMP commands for notifications (LSTN, NTFY) with separate queue IDs and keys

* rename Notifier types

* remove notify key and id from NEW and IDS commands (TODO add other commands)

* fix StoreLog serialization

* add commands for managing notifications

* add notification subscribers to server state, add notifier ID and key to store log

* add notifier ID and key to the queue

* refactor END notification to work for both types of subscriptions, deliver message notification (NMSG)

* process NSUB command - subscribe to message notifications

* test for message notifications

* fix SMP client function for NSUB command

* fix parse/serialize NID command

* refactor use ifM

* check duplicate notifier ID only against other notifier IDs

* refactor getQueue

* test notifier ID and key with store log

* Update src/Simplex/Messaging/Client.hs

Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com>

* Update src/Simplex/Messaging/Server.hs

Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com>

* store log: s/NOTIFY/NOTIFIER/

Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com>
2021-11-14 18:52:29 +00:00
Evgeny Poberezkin 057d5eeb24 AES encription with auth tag after cypher text (the order in WebCrypto etc.); upgrade GHC to 8.10.7 (#197) 2021-10-24 21:03:41 +01:00
Efim Poberezkin bfcbe4bcd8 remove build matrix and create release in the end of build to avoid downtime in binary availability (#196) 2021-10-02 18:49:24 +10:00
Evgeny Poberezkin 6b5427b826 update DigitalOcean image manifest and init script to install the latest server release, add docs (#195) 2021-09-12 14:21:09 +01:00
Evgeny Poberezkin 35e6593581 0.4.1 2021-09-11 18:55:31 +01:00
Evgeny Poberezkin 33de5f6fec include migrations in the package, update docs, versions (#194)
* include migrations in the package, update versions

* update DigitalOcean version
2021-09-11 18:36:12 +01:00
Evgeny Poberezkin d1d7d51fe0 0.4.0 2021-09-11 12:03:51 +01:00
Evgeny Poberezkin 0f9226ba2a update version (0.4.0) and changelog (#193)
* update version (0.4.0) and changelog

* changelog corrections

* make tests more stable
2021-09-11 10:26:51 +01:00
Efim Poberezkin 862f25500d v4 (#192) 2021-09-09 01:30:39 +10:00
Efim Poberezkin da071a0abf Merge pull request #160 from simplex-chat/v4
v4
2021-09-09 01:19:01 +10:00
Efim Poberezkin 51491d9cd5 Merge branch 'master' into v4 2021-09-09 01:10:41 +10:00
Evgeny PoberezkinandEfim Poberezkin d973c834a5 update protocols (#190)
Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com>
2021-09-08 00:25:04 +10:00
Evgeny Poberezkin e07bedac0e move tryError to Simplex.Messaging.Util (#191) 2021-09-04 19:01:54 +01:00
Evgeny Poberezkin 2ac903a2dd do not allow "," and ";" separators in smp-server hostname (#189) 2021-09-03 20:35:48 +01:00
Evgeny Poberezkin d23254f44a SMP block size via config, the client can override size set by the server (#188) 2021-09-03 20:16:10 +01:00
Evgeny Poberezkin 9a3afda290 ACK command, ackMessage, do not send ACK to SMP server on MSG (after the initial handshake) (#187) 2021-09-01 08:29:12 +01:00
Evgeny Poberezkin 68f24f90d1 export withAgentLock and do not acquire agent lock in functional API (#186) 2021-08-30 18:29:13 +01:00
Evgeny Poberezkin 75259d70cf set block size to 4096 bytes (#185) 2021-08-30 10:47:22 +01:00
Evgeny Poberezkin 96c6339306 fix error handling of sending pending messages; set block size to 8192 (#184)
* fix error handling of sending pending messages

* s/tryAction/tryError/
2021-08-29 13:10:59 +01:00
Evgeny Poberezkin 7b5ebf7bd2 change type of agent sendMessage result to AgentMsgId (#183) 2021-08-26 23:25:49 +01:00
Evgeny Poberezkin cb950ae2e4 add SMP queue quota to server config (and use TBQueue for messages) (#182) 2021-08-26 22:54:51 +01:00
Evgeny Poberezkin 26b0edabfc refactor with RecordWildCards (#181) 2021-08-22 13:19:38 +01:00
Evgeny Poberezkin cda02a3b30 support 3072 bits RSA key size (#180) 2021-08-22 10:04:49 +01:00
Evgeny Poberezkin dd5137c336 asynchronous message sending to allow accepting messages when agent (or server) is offline (#179)
* asynchronous message sending to allow accepting messages when agent (or server) is offline

* send pending messages after agent restart; test for pending messages delivery when server restarted

* test pending message delivery after agent restart

* message delivery process per server

* test layout
2021-08-12 22:15:55 +01:00
Evgeny Poberezkin e045774caa reconnect server and resubscribe connections after disconnection (#178) 2021-08-09 08:49:49 +01:00
Evgeny Poberezkin dd6a53b0d2 funding.yml (#177) 2021-08-05 19:47:22 +01:00
Evgeny Poberezkin d9084522af agent lock to avoid running subscriber and client processing in parallel; remove check that the connection is active when delivering a message (#176)
* agent lock to avoid running subscriber and client processing in parallel; remove check that the connection is active when delivering a message

* use agent lock in agent functions
2021-08-05 08:33:48 +01:00
Evgeny Poberezkin 3829479a21 Remove introductions, rename CONF/LET to REQ/ACPT (#175)
* remove intros and functions in Reader monad

* rename CONF/LET to REQ/ACPT, allowConnection to acceptConnection
2021-08-05 08:27:44 +01:00
Efim PoberezkinandEvgeny Poberezkin 8a4bced569 decrease initial delay for HELLO retries on online activation (#174)
* decrease initial delay for HELLO retries on online activation

* move retry interval to config

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2021-08-01 13:51:40 +01:00
Nikita Poberezkin 8ac4b77777 create cfg dir before server initialization (#173) 2021-07-25 20:29:47 +01:00
Evgeny Poberezkin 30c36b488a commit simplexmq.cabal to remove stack DEPRECATED warning (#172) 2021-07-25 20:27:09 +01:00
Evgeny Poberezkin 10fcb9771a use explicit immediate transactions in all functions that change db (#170) 2021-07-20 21:21:30 +01:00
Nikita Poberezkin 65782d7ef6 add digital ocean deployment instruction (#168)
* add digital ocean deployment instruction

* fix instruction

* fix image link and remove ssh instruction link
2021-07-12 17:22:35 +01:00
Efim Poberezkin 5e380e1d47 add tests for establishing connection asynchronously; retrieve correct key for activation on restart (#169) 2021-07-10 20:02:48 +10:00
Efim Poberezkin 9f945492dd fix markdown error in crypto rfc (#167) 2021-07-09 04:27:07 +10:00
Evgeny Poberezkin 8ba3e3e45a remove connection ID parameter from agent functions (#166)
* remove connection ID parameter from agent functions

* remove unused extension
2021-07-03 20:18:37 +01:00
Efim PoberezkinandEvgeny Poberezkin 3d9ceff691 ask client for confirmation of sender; make establishment of connection asynchronous (#163)
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2021-07-04 04:48:24 +10:00
Evgeny Poberezkin daad3315eb upgrade stack resolver to lts-18.0 (#165)
* upgrade stack resolver to lts-18.0

* fix random package version
2021-06-30 10:35:50 +01:00
Evgeny Poberezkin e4d9b481ec refactor message meta to MsgMeta type (#164) 2021-06-30 10:29:45 +01:00
Evgeny Poberezkin 7af7272635 SQLiteStore connection pool (#162)
* SQLiteStore connection pool

* move SQLiteStore to agent env - single store instance is used for all clients
2021-06-25 18:17:11 +01:00
Evgeny Poberezkin 09c6adeabc pass migration as parameter (to use in simplex-chat) (#161)
* pass migration as parameter (to use in simplex-chat)

* add connId parameter to create/joinConnection
2021-06-24 18:39:59 +01:00
Evgeny Poberezkin d5f324cb5c SMP agent: functional API (#159)
* SMP agent: functional API (WIP)

* functional API for SMP agent, tests

* fix ICON message parameter

* use stateTVar
2021-06-13 11:11:44 +01:00
Evgeny Poberezkin bf5561c89c remove entities, remove broadcasts (#158) 2021-06-12 20:01:36 +01:00
Evgeny Poberezkin ab89963f45 introduction protocol (#156)
* commands to support introduction

* agent messages / envelopes to support introductions

* introductions and invitations table; insert record with random unique ID

* store class methods and types for introductions

* process INTRO and ACPT commands for connection introductions

* fix tests: add MonadFail constraint, remove OK response to JOIN

* process agent messages for introductions

* ICON notification when introduction is completed

* replace multiway if with case

* correction

* support random connection IDs

* save additional connection fields, refactor create connection funcs

* refactor

* refactor

* test duplex connection with random IDs

* store methods for introductions

* test introduction

* fix parsing of CON agent message

* test introduction with random connection IDs

* broadcast with random connection and broadcast IDs

* clean up sql
2021-06-11 21:33:13 +01:00
Evgeny Poberezkin 5ebb546d58 remove digitalocean deployment until ready (#157) 2021-06-06 11:20:06 +01:00
Nikita Poberezkin 46c3589604 add DigitalOcean SMP server Droplet recipe, extend readme (#150) 2021-06-01 23:10:58 +03:00
Evgeny Poberezkin dffa7a6100 groups agent protocol (#142)
* groups agent protocol

* groups to chat protocol

* update groups doc / diagram

* group protocol commands

* update group protocol

* count messages for different group operations

* broadcast and introduction as components for group protocol

* list connections in the broadcast

* protocol commands overview - using polymorphic commands

* another version of group protocol based on "broadcast" and "introduction" concepts

* update number of messages needed to establish connection

* corrections to broadcast and introduction protocols

* corrections in groups protocol

* corrections
2021-06-01 18:20:12 +01:00
Evgeny Poberezkin bc780343df broadcast commands (#154)
* broadcast commands (WIP)

* broadcasts: store and commands implementation

* test broadcast

* broadcast test

* rename migration, handle SEBadConnType errors

* query semicolons
2021-06-01 18:11:16 +01:00
Evgeny Poberezkin 84ce001598 database migrations (#153)
* database migrations

* fix: reverse order of down migrations

* use positional parameters in queries

* simplify migrations

* typo

* rename SchemaMigration to Migration

* move store initialization to IO monad
2021-05-31 16:38:35 +01:00
Evgeny Poberezkin 92bd8ef335 agent entity types (#148)
* agent entity types (WIP - fails)

* agent entities (tests pass, TODO - reduce boilerplate)

* simplify test patters

* simplify test patterns 2

* refactor testEquality for ACommand

* stricter entity parsing and correct serialization, updated tests

* fix check of entity ID

* remove unused instance Eq (Entity t)
2021-05-29 13:01:17 +01:00
Evgeny Poberezkin 85535de7d8 update script link (#152)
* update script link

* linode logo

* update logo image
2021-05-28 19:28:36 +01:00
Nikita Poberezkin 7bbc4e4109 add linode deployment recipe, modify smp-server-linode script (#151) 2021-05-28 21:00:24 +03:00
Nikita Poberezkin 248aa234d1 turn off smp server websockets support in linode script (#149) 2021-05-27 11:48:30 +03:00
Nikita Poberezkin 26a0665d81 SMP server linode StackScript (#147) 2021-05-24 22:28:10 +03:00
97 changed files with 11106 additions and 3615 deletions
+1
View File
@@ -0,0 +1 @@
* @epoberezkin @efim-poberezkin
+2
View File
@@ -0,0 +1,2 @@
github: simplex-chat
open_collective: simplex-chat
+1 -1
View File
@@ -1,4 +1,4 @@
{
"template": "${{UNCATEGORIZED}}",
"pr_template": "- ${{TITLE}}\n"
"pr_template": "- ${{TITLE}}"
}
+44 -49
View File
@@ -4,19 +4,41 @@ on:
push:
branches:
- master
- stable
tags:
- "v*"
pull_request:
jobs:
prepare-release:
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
build:
runs-on: ubuntu-20.04
steps:
- name: Clone project
uses: actions/checkout@v2
- name: Setup Stack
uses: haskell/actions/setup@v1
with:
ghc-version: '8.10.7'
enable-stack: true
stack-version: 'latest'
- name: Cache dependencies
uses: actions/cache@v2
with:
path: ~/.stack
key: ${{ hashFiles('stack.yaml') }}
- name: Build & test
id: build_test
shell: bash
run: |
stack build --test --force-dirty
install_root=$(stack path --local-install-root)
mv ${install_root}/bin/smp-server smp-server-ubuntu-20_04-x86-64
- name: Build changelog
if: startsWith(github.ref, 'refs/tags/v')
id: build_changelog
uses: mikepenz/release-changelog-builder-action@v1
with:
@@ -27,57 +49,30 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Extract release candidate
if: startsWith(github.ref, 'refs/tags/v')
id: extract_release_candidate
shell: bash
run: |
if [[ ${GITHUB_REF} == *rc* ]]; then
echo "::set-output name=release_candidate::true"
else
echo "::set-output name=release_candidate::false"
fi
- name: Create release
if: startsWith(github.ref, 'refs/tags/v')
uses: softprops/action-gh-release@v1
with:
body: ${{ steps.build_changelog.outputs.changelog }}
body: |
See full changelog [here](https://github.com/simplex-chat/simplexmq/blob/master/CHANGELOG.md).
Commits:
${{ steps.build_changelog.outputs.changelog }}
prerelease: ${{ steps.extract_release_candidate.outputs.release_candidate }}
files: |
LICENSE
smp-server-ubuntu-20_04-x86-64
fail_on_unmatched_files: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build:
name: build-${{ matrix.os }}
if: always()
needs: prepare-release
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-20.04
asset_name: smp-server-ubuntu-20_04-x86-64
- os: ubuntu-18.04
asset_name: smp-server-ubuntu-18_04-x86-64
steps:
- name: Clone project
uses: actions/checkout@v2
- name: Setup Stack
uses: haskell/actions/setup@v1
with:
ghc-version: '8.8.4'
enable-stack: true
stack-version: 'latest'
- name: Cache dependencies
uses: actions/cache@v2
with:
path: ~/.stack
key: ${{ matrix.os }}-${{ hashFiles('stack.yaml') }}
- name: Build & test
id: build_test
run: |
stack build --test
echo "::set-output name=LOCAL_INSTALL_ROOT::$(stack path --local-install-root)"
- name: Upload binaries to release
if: startsWith(github.ref, 'refs/tags/v')
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ${{ steps.build_test.outputs.LOCAL_INSTALL_ROOT }}/bin/smp-server
asset_name: ${{ matrix.asset_name }}
tag: ${{ github.ref }}
+3 -1
View File
@@ -1,4 +1,6 @@
*.lock
*.cabal
*.db
*.db.bak
*.session.sql
tests/tmp
dist-newstyle/
+101
View File
@@ -1,3 +1,104 @@
# 1.0.3
SMP server:
- Reduce server message queue quota to 128 messages.
SMP agent:
- Add "yes to migrations" option.
- Make new SMP client attempt to reconnect on network error.
- Reduce connection handshake expiration to 2 days.
JSON encoding of types used in simplex-chat, some other minor adjustments.
# 1.0.2
General:
- Enable TLS 1.3 parameters for TLS handshake (server and client).
- Switch from hs-tls fork to original repo now that it supports getFinished and getPeerFinished APIs for both TLS 1.2 and TLS 1.3.
SMP server:
- Perform TLS handshake in a separate thread per-connection.
SMP agent:
- Cease attempts to send HELLO after one week timeout.
- Coalesce requests to connect to SMP servers, to have 1 connection per server.
# 1.0.1
SMP server:
- Explicitly set line buffering in stdout/stderr to log each line when output is redirected to files.
# 1.0.0
Security and privacy improvements:
- Faster and more secure 2-layer E2E encryption with additional encryption layer between servers and recipients:
- application messages in each duplex connection (managed by SMP agents - see [overview](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md)) are encrypted using [double-ratchet algorithm](https://www.signal.org/docs/specifications/doubleratchet/), providing forward secrecy and break-in recovery. This layer uses two Curve448 keys per client for [X3DH key agreement](https://www.signal.org/docs/specifications/x3dh/), SHA512 based HKDFs and AES-GCM AEAD encryption.
- SMP client messages are additionally E2E encrypted in each SMP queue to avoid cipher-text correlation of messages sent via multiple redundant queues (that will be supported soon). This and the next layer use [NaCl crypto_box algorithm](https://nacl.cr.yp.to/index.html) with XSalsa20Poly1305 cipher and Curve25519 keys for DH key agreement.
- Messages delivered from the servers to the recipients are additionally encrypted to avoid cipher-text correlation between sent and received messages.
- To prevent any traffic correlation by content size, SimpleX uses fixed transport block size of 16kb (16384 bytes) with padding on all encryption layers:
- application messages are padded to 15788 bytes before E2E double-ratchet encryption.
- messages between SMP clients are padded to 16032 bytes before E2E encryption in each SMP queue.
- messages from the server to the recipient are padded to padded to 16088 bytes before the additional encryption layer (see above).
- TLS 1.2+ with tls-unique channel binding in each command to prevent replay attacks.
- Server identity verification via server offline certificate fingerprints included in SMP server addresses.
New functionality:
- Support for notification servers with new SMP commands: `NKEY`/`NID`, `NSUB`/`NMSG`.
Efficiency improvements:
- Binary protocol encodings to reduce overhead from circa 15% to approximately 3.7% of transmitted application message size, with only 2.2% overhead for SMP protocol messages.
- More performant cryptographic algorithms.
For more information about SimpleX:
- [SimpleX overview](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md).
- [SimpleX chat v1 announcement](https://github.com/simplex-chat/simplex-chat/blob/master/blog/20220112-simplex-chat-v1-released.md).
# 0.5.2
- Fix message delivery logic that blocked delivery of all server messages when server per-queue quota exceeded, making it concurrent per SMP queue, not per server.
# 0.5.1
- Fix server subscription logic bug that was leading to memory leak / resource exhaustion in some edge cases.
# 0.5.0
- No changes in SMP server implementation - it is backwards compatible with v0.4.1
- SMP agent changes:
- URI syntax for SMP queues and connection requests.
- long-term connections links ("contacts") in SMP agent protocol.
- agent command changes:
- `REQ` notification and `ACPT` command are used only with long-term connection links.
- `CONF` notification and `LET` commands are used for normal duplex connections.
# 0.4.1
- Include migrations in the package
# 0.4.0
- SMP server implementation and [SMP protocol](https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md) changes:
- support 3072 bit RSA key size
- add SMP queue quotas
- set default transport block size to 4096 bits
- allow SMP client to change transport block size during transport connection handshake
- SMP agent implementation and protocol changes:
- additional connection confirmation step for initiating party
- automatically resume subscribed duplex connections once transport connection is resumed
- passing an arbitrary binary information between parties during the duplex connection handshake - can be used to identify parties
- asynchronous duplex connection handshake - the parties do not have to be online at the same time
- asynchronous message delivery - the agent does not need transport connection to accept client messages for delivery
- additional confirmation of message reception from the client to prevent message loss in case of process termination
- set transport block size to 8192 bits (in the future the agent protocol can allow to have different block sizes for different duplex connections)
- added client commands and notifications (see [agent protocol](https://github.com/simplex-chat/simplexmq/blob/master/protocol/agent-protocol.md)):
- `REQ` - the notification about joining party establishing connection
- `ACPT` - the command to accept connection with the joining party
- `INFO` - the notification with the information from the initiating party
- `DOWN`/`UP` - the notifications about losing/resuming the connection
- `ACK` - the command to confirm that the message reception/processing is complete
- `MID` - the response to `SEND` confirming that the message is accepted by the agent
- `MERR` - the notification about permanent message delivery error (e.g., `ERR AUTH` indicating that the queue was removed)
# 0.3.2
- Support websockets
+92 -11
View File
@@ -3,34 +3,61 @@
[![GitHub build](https://github.com/simplex-chat/simplexmq/workflows/build/badge.svg)](https://github.com/simplex-chat/simplexmq/actions?query=workflow%3Abuild)
[![GitHub release](https://img.shields.io/github/v/release/simplex-chat/simplexmq)](https://github.com/simplex-chat/simplexmq/releases)
📢 SimpleXMQ v1 is released - with many security, privacy and efficiency improvements, new functionality - see [release notes](https://github.com/simplex-chat/simplexmq/releases/tag/v1.0.0).
**Please note**: v1 is not backwards compatible, but it has the version negotiation built into all protocol layers for forwards compatibility of this version and backwards compatibility of the future versions, that will be backwards compatible for at least two versions back.
If you have a server deployed please deploy a new server to a new host and retire the previous version once it is no longer used.
## Message broker for unidirectional (simplex) queues
SimpleXMQ is a message broker for managing message queues and sending messages over public network. It consists of SMP server, SMP client library and SMP agent that implement [SMP protocol](https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md) for client-server communication and [SMP agent protocol](https://github.com/simplex-chat/simplexmq/blob/master/protocol/agent-protocol.md) to manage duplex connections via simplex queues on multiple SMP servers.
SMP protocol is inspired by [Redis serialization protocol](https://redis.io/topics/protocol), but it is much simpler - it currently has only 8 client commands and 6 server responses.
SMP protocol is inspired by [Redis serialization protocol](https://redis.io/topics/protocol), but it is much simpler - it currently has only 10 client commands and 8 server responses.
SimpleXMQ is implemented in Haskell - it benefits from robust software transactional memory (STM) and concurrency primitives that Haskell provides.
## SimpleXMQ roadmap
- Streams - high performance message queues. See [Streams RFC](https://github.com/simplex-chat/simplexmq/blob/master/rfcs/2021-02-28-streams.md) for details.
- "Small" connection groups, when each message will be sent by the SMP agent to multiple connections with a single client command. See [Groups RFC](https://github.com/simplex-chat/simplexmq/blob/master/rfcs/2021-03-18-groups.md) for details.
- SMP agents cluster to share connections and message management by multiple agents (for example, it would enable multi-device use for [simplex-chat](https://github.com/simplex-chat/simplex-chat)).
- SMP queue redundancy and rotation in SMP agent duplex connections.
- "Large" groups design and implementation.
- SimpleX service protocol and application template - to enable users building services and chat bots that work over SimpleX protocol stack. The first such service will be a notification service for a mobile app.
- SMP queue redundancy and rotation in SMP agent connections.
- SMP agents synchronization to share connections and messages between multiple agents (it would allow using multiple devices for [simplex-chat](https://github.com/simplex-chat/simplex-chat)).
## Components
### SMP server
[SMP server](https://github.com/simplex-chat/simplexmq/blob/master/apps/smp-server/Main.hs) can be run on any Linux distribution without any dependencies. It uses in-memory persistence with an optional append-only log of created queues that allows to re-start the server without losing the connections. This log is compacted on every server restart, permanently removing suspended and removed queues.
[SMP server](https://github.com/simplex-chat/simplexmq/blob/master/apps/smp-server/Main.hs) can be run on any Linux distribution without any dependencies, including low power/low memory devices.
To enable the queue logging, uncomment `enable: on` option in `smp-server.ini` configuration file that is created the first time the server is started.
To initialize the server use `smp-server init` command - it will generate keys and certificates for TLS transport. The fingerprint of offline certificate is used as part of the server address to protect client/server connection against man-in-the-middle attacks: `smp://<fingerprint>@<hostname>[:5223]`.
On the first start the server generates an RSA key pair for encrypted transport handshake and outputs hash of the public key every time it runs - this hash should be used as part of the server address: `<hostname>:5223#<key hash>`.
SMP server uses in-memory persistence with an optional append-only log of created queues that allows to re-start the server without losing the connections. This log is compacted on every server restart, permanently removing suspended and removed queues.
To enable store log, initialize server using `smp-server -l` command, or modify `smp-server.ini` created during initialization (uncomment `enable: on` option in the store log section). Use `smp-server --help` for other usage tips.
> **Please note:** On initialization SMP server creates a chain of two certificates: a self-signed CA certificate ("offline") and a server certificate used for TLS handshake ("online"). **You should store CA certificate private key securely and delete it from the server. If server TLS credential is compromised this key can be used to sign a new one, keeping the same server identity and established connections.** CA private key location by default is `/etc/opt/simplex/ca.key`.
SMP server implements [SMP protocol](https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md).
#### Running SMP server on MacOS
SMP server requires OpenSSL library for initialization. On MacOS OpenSSL library may be replaced with LibreSSL, which doesn't support required algorithms. Before initializing SMP server verify you have OpenSSL installed:
```sh
openssl version
```
If it says "LibreSSL", please install original OpenSSL:
```sh
brew update
brew install openssl
echo 'PATH="/opt/homebrew/opt/openssl@3/bin:$PATH"' >> ~/.zprofile # or follow whatever instructions brew suggests
. ~/.zprofile # or restart your terminal to start a new session
```
Now `openssl version` should be saying "OpenSSL". You can now run `smp-server init` to initialize your SMP server.
### SMP client library
[SMP client](https://github.com/simplex-chat/simplexmq/blob/master/src/Simplex/Messaging/Client.hs) is a Haskell library to connect to SMP servers that allows to:
@@ -50,12 +77,66 @@ See [simplex-chat](https://github.com/simplex-chat/simplex-chat) terminal UI for
## Using SMP server and SMP agent
You can either run SMP server locally or try local SMP agent with the deployed demo server:
You can either run your own SMP server locally or deploy using [Linode StackScript](https://cloud.linode.com/stackscripts/748014), or try local SMP agent with the deployed servers:
`smp1.simplex.im:5223#pLdiGvm0jD1CMblnov6Edd/391OrYsShw+RgdfR0ChA=`
`smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im`
`smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im`
`smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im`
It's the easiest to try SMP agent via a prototype [simplex-chat](https://github.com/simplex-chat/simplex-chat) terminal UI.
[<img alt="Linode" src="https://raw.githubusercontent.com/simplex-chat/simplexmq/master/img/linode.svg" align="right" width="200">](https://cloud.linode.com/stackscripts/748014)
## Deploy SMP server on Linode
\* You can use free credit Linode offers when [creating a new account](https://www.linode.com/) to deploy an SMP server.
Deployment on Linode is performed via StackScripts, which serve as recipes for Linode instances, also called Linodes. To deploy SMP server on Linode:
- Create a Linode account or login with an already existing one.
- Open [SMP server StackScript](https://cloud.linode.com/stackscripts/748014) and click "Deploy New Linode".
- You can optionally configure the following parameters:
- SMP Server store log flag for queue persistence on server restart, recommended.
- [Linode API token](https://www.linode.com/docs/guides/getting-started-with-the-linode-api#get-an-access-token) to attach server address etc. as tags to Linode and to add A record to your 2nd level domain (e.g. `example.com` [domain should be created](https://cloud.linode.com/domains/create) in your account prior to deployment). The API token access scopes:
- read/write for "linodes"
- read/write for "domains"
- Domain name to use instead of Linode IP address, e.g. `smp1.example.com`.
- Choose the region and plan, Shared CPU Nanode with 1Gb is sufficient.
- Provide ssh key to be able to connect to your Linode via ssh. If you haven't provided a Linode API token this step is required to login to your Linode and get the server's fingerprint either from the welcome message or from the file `/etc/opt/simplex/fingerprint` after server starts. See [Linode's guide on ssh](https://www.linode.com/docs/guides/use-public-key-authentication-with-ssh/) .
- Deploy your Linode. After it starts wait for SMP server to start and for tags to appear (if a Linode API token was provided). It may take up to 5 minutes depending on the connection speed on the Linode. Connecting Linode IP address to provided domain name may take some additional time.
- Get `address` and `fingerprint` either from Linode tags (click on a tag and copy it's value from the browser search panel) or via ssh.
- Great, your own SMP server is ready! If you provided FQDN use `smp://<fingerprint>@<fqdn>` as SMP server address in the client, otherwise use `smp://<fingerprint>@<ip_address>`.
Please submit an [issue](https://github.com/simplex-chat/simplexmq/issues) if any problems occur.
[<img alt="DigitalOcean" src="https://raw.githubusercontent.com/simplex-chat/simplexmq/master/img/digitalocean.png" align="right" width="300">](https://marketplace.digitalocean.com/apps/simplex-server)
## Deploy SMP server on DigitalOcean
> 🚧 DigitalOcean snapshot is currently not up to date, it will soon be updated 🏗️
\* When creating a DigitalOcean account you can use [this link](https://try.digitalocean.com/freetrialoffer/) to get free credit. (You would still be required either to provide your credit card details or make a confirmation pre-payment with PayPal)
To deploy SMP server use [SimpleX Server 1-click app](https://marketplace.digitalocean.com/apps/simplex-server) from DigitalOcean marketplace:
- Create a DigitalOcean account or login with an already existing one.
- Click 'Create SimpleX server Droplet' button.
- Choose the region and plan according to your requirements (Basic plan should be sufficient).
- Finalize Droplet creation.
- Open "Console" on your Droplet management page to get SMP server fingerprint - either from the welcome message or from `/etc/opt/simplex/fingerprint`. Alternatively you can manually SSH to created Droplet, see [DigitalOcean instruction](https://docs.digitalocean.com/products/droplets/how-to/connect-with-ssh/).
- Great, your own SMP server is ready! Use `smp://<fingerprint>@<ip_address>` as SMP server address in the client.
Please submit an [issue](https://github.com/simplex-chat/simplexmq/issues) if any problems occur.
> **Please note:** SMP server uses server address as a Common Name for server certificate generated during initialization. If you would like your server address to be FQDN instead of IP address, you can log in to your Droplet and run the commands below to re-initialize the server. Alternatively you can use [Linode StackScript](https://cloud.linode.com/stackscripts/748014) which allows this parameterization.
```sh
smp-server delete
smp-server init [-l] -n <fqdn>
```
## SMP server design
![SMP server design](https://raw.githubusercontent.com/simplex-chat/simplexmq/master/design/server.svg)
+4 -14
View File
@@ -6,22 +6,12 @@ module Main where
import Control.Logger.Simple
import qualified Data.List.NonEmpty as L
import Simplex.Messaging.Agent (runSMPAgent)
import Simplex.Messaging.Agent.Env.SQLite
import Simplex.Messaging.Client (smpDefaultConfig)
import Simplex.Messaging.Transport (TCP, Transport (..))
import Simplex.Messaging.Agent.Server (runSMPAgent)
import Simplex.Messaging.Transport (TLS, Transport (..))
cfg :: AgentConfig
cfg =
AgentConfig
{ tcpPort = "5224",
smpServers = L.fromList ["localhost:5223#KXNE1m2E1m0lm92WGKet9CL6+lO742Vy5G6nsrkvgs8="],
rsaKeySize = 2048 `div` 8,
connIdBytes = 12,
tbqSize = 16,
dbFile = "smp-agent.db",
smpCfg = smpDefaultConfig
}
cfg = defaultAgentConfig {smpServers = L.fromList ["smp://bU0K-bRg24xWW__lS0umO1Zdw_SXqpJNtm1_RrPLViE=@localhost:5223"]}
logCfg :: LogConfig
logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
@@ -30,4 +20,4 @@ main :: IO ()
main = do
putStrLn $ "SMP agent listening on port " ++ tcpPort (cfg :: AgentConfig)
setLogLevel LogInfo -- LogError
withGlobalLogging logCfg $ runSMPAgent (transport @TCP) cfg
withGlobalLogging logCfg $ runSMPAgent (transport @TLS) cfg
+271 -216
View File
@@ -1,5 +1,6 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
@@ -8,45 +9,28 @@
module Main where
import Control.Monad.Except
import Control.Monad.Trans.Except
import qualified Crypto.Store.PKCS8 as S
import Data.ByteString.Base64 (encode)
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Char (toLower)
import Data.Either (fromRight)
import Data.Ini (Ini, lookupValue, readIniFile)
import Data.Text (Text)
import Data.Maybe (fromMaybe)
import qualified Data.Text as T
import Data.X509 (PrivKey (PrivKeyRSA))
import Network.Socket (ServiceName)
import Data.X509.Validation (Fingerprint (..))
import Network.Socket (HostName, ServiceName)
import Options.Applicative
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Server (runSMPServer)
import Simplex.Messaging.Server.Env.STM
import Simplex.Messaging.Server.StoreLog (StoreLog, openReadStoreLog, storeLogFilePath)
import Simplex.Messaging.Transport (ATransport (..), TCP, Transport (..))
import Simplex.Messaging.Transport (ATransport (..), TLS, Transport (..), simplexMQVersion)
import Simplex.Messaging.Transport.Server (loadFingerprint)
import Simplex.Messaging.Transport.WebSockets (WS)
import System.Directory (createDirectoryIfMissing, doesFileExist, removeFile)
import System.Directory (createDirectoryIfMissing, doesDirectoryExist, doesFileExist, removeDirectoryRecursive)
import System.Exit (exitFailure)
import System.FilePath (combine)
import System.IO (IOMode (..), hFlush, stdout)
defaultServerPort :: ServiceName
defaultServerPort = "5223"
serverConfig :: ServerConfig
serverConfig =
ServerConfig
{ tbqSize = 16,
queueIdBytes = 12,
msgIdBytes = 6,
-- below parameters are set based on ini file /etc/opt/simplex/smp-server.ini
transports = undefined,
storeLog = undefined,
serverPrivateKey = undefined
}
newKeySize :: Int
newKeySize = 2048 `div` 8
import System.IO (BufferMode (..), IOMode (..), hGetLine, hSetBuffering, stderr, stdout, withFile)
import System.Process (readCreateProcess, shell)
import Text.Read (readMaybe)
cfgDir :: FilePath
cfgDir = "/etc/opt/simplex"
@@ -54,211 +38,282 @@ cfgDir = "/etc/opt/simplex"
logDir :: FilePath
logDir = "/var/opt/simplex"
defaultStoreLogFile :: FilePath
defaultStoreLogFile = combine logDir "smp-server-store.log"
iniFile :: FilePath
iniFile = combine cfgDir "smp-server.ini"
defaultKeyFile :: FilePath
defaultKeyFile = combine cfgDir "server_key"
storeLogFile :: FilePath
storeLogFile = combine logDir "smp-server-store.log"
caKeyFile :: FilePath
caKeyFile = combine cfgDir "ca.key"
caCrtFile :: FilePath
caCrtFile = combine cfgDir "ca.crt"
serverKeyFile :: FilePath
serverKeyFile = combine cfgDir "server.key"
serverCrtFile :: FilePath
serverCrtFile = combine cfgDir "server.crt"
fingerprintFile :: FilePath
fingerprintFile = combine cfgDir "fingerprint"
main :: IO ()
main = do
opts <- getServerOpts
case serverCommand opts of
ServerInit ->
runExceptT (getConfig opts) >>= \case
Right cfg -> do
putStrLn "Error: server is already initialized. Start it with `smp-server start` command"
printConfig cfg
exitFailure
Left _ -> do
cfg <- initializeServer opts
putStrLn "Server was initialized. Start it with `smp-server start` command"
printConfig cfg
ServerStart ->
runExceptT (getConfig opts) >>= \case
Right cfg -> runServer cfg
Left e -> do
putStrLn $ "Server is not initialized: " <> e
putStrLn "Initialize server with `smp-server init` command"
exitFailure
ServerDelete -> do
deleteServer
putStrLn "Server key, config file and store log deleted"
getCliCommand >>= \case
Init opts ->
doesFileExist iniFile >>= \case
True -> exitError $ "Error: server is already initialized (" <> iniFile <> " exists).\nRun `smp-server start`."
_ -> initializeServer opts
Start ->
doesFileExist iniFile >>= \case
True -> readIniFile iniFile >>= either exitError (runServer . mkIniOptions)
_ -> exitError $ "Error: server is not initialized (" <> iniFile <> " does not exist).\nRun `smp-server init`."
Delete -> cleanup >> putStrLn "Deleted configuration and log files"
getConfig :: ServerOpts -> ExceptT String IO ServerConfig
getConfig opts = do
ini <- readIni
pk <- readKey ini
storeLog <- liftIO $ openStoreLog opts ini
pure $ makeConfig ini pk storeLog
exitError :: String -> IO ()
exitError msg = putStrLn msg >> exitFailure
makeConfig :: IniOpts -> C.FullPrivateKey -> Maybe (StoreLog 'ReadMode) -> ServerConfig
makeConfig IniOpts {serverPort, enableWebsockets} pk storeLog =
let transports = (serverPort, transport @TCP) : [("80", transport @WS) | enableWebsockets]
in serverConfig {serverPrivateKey = pk, storeLog, transports}
data CliCommand
= Init InitOptions
| Start
| Delete
printConfig :: ServerConfig -> IO ()
printConfig ServerConfig {serverPrivateKey, storeLog} = do
B.putStrLn $ "transport key hash: " <> serverKeyHash serverPrivateKey
putStrLn $ case storeLog of
Just s -> "store log: " <> storeLogFilePath s
Nothing -> "store log disabled"
initializeServer :: ServerOpts -> IO ServerConfig
initializeServer opts = do
ini <- createIni opts
pk <- createKey ini
storeLog <- openStoreLog opts ini
pure $ makeConfig ini pk storeLog
runServer :: ServerConfig -> IO ()
runServer cfg = do
printConfig cfg
forM_ (transports cfg) $ \(port, ATransport t) ->
putStrLn $ "listening on port " <> port <> " (" <> transportName t <> ")"
runSMPServer cfg
deleteServer :: IO ()
deleteServer = do
ini <- runExceptT readIni
deleteIfExists iniFile
case ini of
Right IniOpts {storeLogFile, serverKeyFile} -> do
deleteIfExists storeLogFile
deleteIfExists serverKeyFile
Left _ -> do
deleteIfExists defaultKeyFile
deleteIfExists defaultStoreLogFile
data IniOpts = IniOpts
data InitOptions = InitOptions
{ enableStoreLog :: Bool,
storeLogFile :: FilePath,
serverKeyFile :: FilePath,
serverPort :: ServiceName,
signAlgorithm :: SignAlgorithm,
ip :: HostName,
fqdn :: Maybe HostName
}
deriving (Show)
data SignAlgorithm = ED448 | ED25519
deriving (Read, Show)
getCliCommand :: IO CliCommand
getCliCommand =
customExecParser
(prefs showHelpOnEmpty)
( info
(helper <*> versionOption <*> cliCommandP)
(header version <> fullDesc)
)
where
versionOption = infoOption version (long "version" <> short 'v' <> help "Show version")
cliCommandP :: Parser CliCommand
cliCommandP =
hsubparser
( command "init" (info initP (progDesc $ "Initialize server - creates " <> cfgDir <> " and " <> logDir <> " directories and configuration files"))
<> command "start" (info (pure Start) (progDesc $ "Start server (configuration: " <> iniFile <> ")"))
<> command "delete" (info (pure Delete) (progDesc "Delete configuration and log files"))
)
where
initP :: Parser CliCommand
initP =
Init
<$> ( InitOptions
<$> switch
( long "store-log"
<> short 'l'
<> help "Enable store log for SMP queues persistence"
)
<*> option
(maybeReader readMaybe)
( long "sign-algorithm"
<> short 'a'
<> help "Signature algorithm used for TLS certificates: ED25519, ED448"
<> value ED448
<> showDefault
<> metavar "ALG"
)
<*> strOption
( long "ip"
<> help
"Server IP address used as Subject Alternative Name for TLS online certificate, \
\also used as Common Name if FQDN is not supplied"
<> value "127.0.0.1"
<> showDefault
<> metavar "IP"
)
<*> (optional . strOption)
( long "fqdn"
<> short 'n'
<> help "Server FQDN used as Common Name and Subject Alternative Name for TLS online certificate"
<> showDefault
<> metavar "FQDN"
)
)
initializeServer :: InitOptions -> IO ()
initializeServer InitOptions {enableStoreLog, signAlgorithm, ip, fqdn} = do
cleanup
createDirectoryIfMissing True cfgDir
createDirectoryIfMissing True logDir
createX509
fp <- saveFingerprint
createIni
putStrLn $ "Server initialized, you can modify configuration in " <> iniFile <> ".\nRun `smp-server start` to start server."
printServiceInfo fp
warnCAPrivateKeyFile
where
createX509 = do
createOpensslCaConf
createOpensslServerConf
-- CA certificate (identity/offline)
run $ "openssl genpkey -algorithm " <> show signAlgorithm <> " -out " <> caKeyFile
run $ "openssl req -new -x509 -days 999999 -config " <> opensslCaConfFile <> " -extensions v3 -key " <> caKeyFile <> " -out " <> caCrtFile
-- server certificate (online)
run $ "openssl genpkey -algorithm " <> show signAlgorithm <> " -out " <> serverKeyFile
run $ "openssl req -new -config " <> opensslServerConfFile <> " -reqexts v3 -key " <> serverKeyFile <> " -out " <> serverCsrFile
run $ "openssl x509 -req -days 999999 -extfile " <> opensslServerConfFile <> " -extensions v3 -in " <> serverCsrFile <> " -CA " <> caCrtFile <> " -CAkey " <> caKeyFile <> " -CAcreateserial -out " <> serverCrtFile
where
run cmd = void $ readCreateProcess (shell cmd) ""
opensslCaConfFile = combine cfgDir "openssl_ca.conf"
opensslServerConfFile = combine cfgDir "openssl_server.conf"
serverCsrFile = combine cfgDir "server.csr"
createOpensslCaConf =
writeFile
opensslCaConfFile
"[req]\n\
\distinguished_name = req_distinguished_name\n\
\prompt = no\n\n\
\[req_distinguished_name]\n\
\CN = SMP server CA\n\
\O = SimpleX\n\n\
\[v3]\n\
\subjectKeyIdentifier = hash\n\
\authorityKeyIdentifier = keyid:always\n\
\basicConstraints = critical,CA:true\n"
-- TODO revise https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.3, https://www.rfc-editor.org/rfc/rfc3279#section-2.3.5
-- IP and FQDN can't both be used as server address interchangeably even if IP is added
-- as Subject Alternative Name, unless the following validation hook is disabled:
-- https://hackage.haskell.org/package/x509-validation-1.6.10/docs/src/Data-X509-Validation.html#validateCertificateName
createOpensslServerConf =
writeFile
opensslServerConfFile
( "[req]\n\
\distinguished_name = req_distinguished_name\n\
\prompt = no\n\n\
\[req_distinguished_name]\n"
<> ("CN = " <> cn <> "\n\n")
<> "[v3]\n\
\basicConstraints = CA:FALSE\n\
\keyUsage = digitalSignature, nonRepudiation, keyAgreement\n\
\extendedKeyUsage = serverAuth\n"
)
where
cn = fromMaybe ip fqdn
saveFingerprint = do
Fingerprint fp <- loadFingerprint caCrtFile
withFile fingerprintFile WriteMode (`B.hPutStrLn` strEncode fp)
pure fp
createIni = do
writeFile iniFile $
"[STORE_LOG]\n\
\# The server uses STM memory to store SMP queues and messages,\n\
\# that will be lost on restart (e.g., as with redis).\n\
\# This option enables saving SMP queues to append only log,\n\
\# and restoring them when the server is started.\n\
\# Log is compacted on start (deleted queues are removed).\n\
\# The messages in the queues are not logged.\n"
<> ("enable: " <> (if enableStoreLog then "on" else "off # on") <> "\n\n")
<> "[TRANSPORT]\n\
\port: 5223\n\
\websockets: off\n"
warnCAPrivateKeyFile =
putStrLn $
"----------\n\
\You should store CA private key securely and delete it from the server.\n\
\If server TLS credential is compromised this key can be used to sign a new one, \
\keeping the same server identity and established connections.\n\
\CA private key location:\n"
<> caKeyFile
<> "\n----------"
data IniOptions = IniOptions
{ enableStoreLog :: Bool,
port :: ServiceName,
enableWebsockets :: Bool
}
readIni :: ExceptT String IO IniOpts
readIni = do
fileExists iniFile
ini <- ExceptT $ readIniFile iniFile
let enableStoreLog = (== Right "on") $ lookupValue "STORE_LOG" "enable" ini
storeLogFile = opt defaultStoreLogFile "STORE_LOG" "file" ini
serverKeyFile = opt defaultKeyFile "TRANSPORT" "key_file" ini
serverPort = opt defaultServerPort "TRANSPORT" "port" ini
enableWebsockets = (== Right "on") $ lookupValue "TRANSPORT" "websockets" ini
pure IniOpts {enableStoreLog, storeLogFile, serverKeyFile, serverPort, enableWebsockets}
-- TODO ? properly parse ini as a whole
mkIniOptions :: Ini -> IniOptions
mkIniOptions ini =
IniOptions
{ enableStoreLog = (== "on") $ strict "STORE_LOG" "enable",
port = T.unpack $ strict "TRANSPORT" "port",
enableWebsockets = (== "on") $ strict "TRANSPORT" "websockets"
}
where
opt :: String -> Text -> Text -> Ini -> String
opt def section key ini = either (const def) T.unpack $ lookupValue section key ini
strict :: String -> String -> T.Text
strict section key =
fromRight (error ("no key " <> key <> " in section " <> section)) $
lookupValue (T.pack section) (T.pack key) ini
createIni :: ServerOpts -> IO IniOpts
createIni ServerOpts {enableStoreLog} = do
writeFile iniFile $
"[STORE_LOG]\n\
\# The server uses STM memory to store SMP queues and messages,\n\
\# that will be lost on restart (e.g., as with redis).\n\
\# This option enables saving SMP queues to append only log,\n\
\# and restoring them when the server is started.\n\
\# Log is compacted on start (deleted queues are removed).\n\
\# The messages in the queues are not logged.\n\n"
<> (if enableStoreLog then "" else "# ")
<> "enable: on\n\
\# file: "
<> defaultStoreLogFile
<> "\n\n\
\[TRANSPORT]\n\n\
\# key_file: "
<> defaultKeyFile
<> "\n\
\# port: "
<> defaultServerPort
<> "\n\
\websockets: on\n"
pure
IniOpts
{ enableStoreLog,
storeLogFile = defaultStoreLogFile,
serverKeyFile = defaultKeyFile,
serverPort = defaultServerPort,
enableWebsockets = True
}
readKey :: IniOpts -> ExceptT String IO C.FullPrivateKey
readKey IniOpts {serverKeyFile} = do
fileExists serverKeyFile
liftIO (S.readKeyFile serverKeyFile) >>= \case
[S.Unprotected (PrivKeyRSA pk)] -> pure $ C.FullPrivateKey pk
[_] -> err "not RSA key"
[] -> err "invalid key file format"
_ -> err "more than one key"
runServer :: IniOptions -> IO ()
runServer IniOptions {enableStoreLog, port, enableWebsockets} = do
hSetBuffering stdout LineBuffering
hSetBuffering stderr LineBuffering
fp <- checkSavedFingerprint
printServiceInfo fp
storeLog <- openStoreLog
let cfg = mkServerConfig storeLog
printServerConfig cfg
runSMPServer cfg
where
err :: String -> ExceptT String IO b
err e = throwE $ e <> ": " <> serverKeyFile
checkSavedFingerprint = do
savedFingerprint <- loadSavedFingerprint
Fingerprint fp <- loadFingerprint caCrtFile
when (B.pack savedFingerprint /= strEncode fp) $
exitError "Stored fingerprint is invalid."
pure fp
createKey :: IniOpts -> IO C.FullPrivateKey
createKey IniOpts {serverKeyFile} = do
createDirectoryIfMissing True cfgDir
(_, pk) <- C.generateKeyPair newKeySize
S.writeKeyFile S.TraditionalFormat serverKeyFile [PrivKeyRSA $ C.rsaPrivateKey pk]
pure pk
mkServerConfig storeLog =
ServerConfig
{ transports = (port, transport @TLS) : [("80", transport @WS) | enableWebsockets],
tbqSize = 16,
serverTbqSize = 64,
msgQueueQuota = 128,
queueIdBytes = 24,
msgIdBytes = 24, -- must be at least 24 bytes, it is used as 192-bit nonce for XSalsa20
caCertificateFile = caCrtFile,
privateKeyFile = serverKeyFile,
certificateFile = serverCrtFile,
storeLog
}
fileExists :: FilePath -> ExceptT String IO ()
fileExists path = do
exists <- liftIO $ doesFileExist path
unless exists . throwE $ "file " <> path <> " not found"
openStoreLog :: IO (Maybe (StoreLog 'ReadMode))
openStoreLog =
if enableStoreLog
then Just <$> openReadStoreLog storeLogFile
else pure Nothing
deleteIfExists :: FilePath -> IO ()
deleteIfExists path = doesFileExist path >>= (`when` removeFile path)
printServerConfig ServerConfig {storeLog, transports} = do
putStrLn $ case storeLog of
Just s -> "Store log: " <> storeLogFilePath s
Nothing -> "Store log disabled."
forM_ transports $ \(p, ATransport t) ->
putStrLn $ "Listening on port " <> p <> " (" <> transportName t <> ")..."
confirm :: String -> IO ()
confirm msg = do
putStr $ msg <> " (y/N): "
hFlush stdout
ok <- getLine
when (map toLower ok /= "y") exitFailure
serverKeyHash :: C.FullPrivateKey -> B.ByteString
serverKeyHash = encode . C.unKeyHash . C.publicKeyHash . C.publicKey
openStoreLog :: ServerOpts -> IniOpts -> IO (Maybe (StoreLog 'ReadMode))
openStoreLog ServerOpts {enableStoreLog = l} IniOpts {enableStoreLog = l', storeLogFile = f}
| l || l' = do
createDirectoryIfMissing True logDir
Just <$> openReadStoreLog f
| otherwise = pure Nothing
data ServerOpts = ServerOpts
{ serverCommand :: ServerCommand,
enableStoreLog :: Bool
}
data ServerCommand = ServerInit | ServerStart | ServerDelete
serverOpts :: Parser ServerOpts
serverOpts =
ServerOpts
<$> subparser
( command "init" (info (pure ServerInit) (progDesc "Initialize server: generate server key and ini file"))
<> command "start" (info (pure ServerStart) (progDesc "Start server (ini: /etc/opt/simplex/smp-server.ini)"))
<> command "delete" (info (pure ServerDelete) (progDesc "Delete server key, ini file and store log"))
)
<*> switch
( long "store-log"
<> short 'l'
<> help "enable store log for SMP queues persistence"
)
getServerOpts :: IO ServerOpts
getServerOpts = customExecParser p opts
cleanup :: IO ()
cleanup = do
deleteDirIfExists cfgDir
deleteDirIfExists logDir
where
p = prefs showHelpOnEmpty
opts =
info
(serverOpts <**> helper)
( fullDesc
<> header "Simplex Messaging Protocol (SMP) Server"
)
deleteDirIfExists path = doesDirectoryExist path >>= (`when` removeDirectoryRecursive path)
printServiceInfo :: ByteString -> IO ()
printServiceInfo fpStr = do
putStrLn version
B.putStrLn $ "Fingerprint: " <> strEncode fpStr
version :: String
version = "SMP server v" <> simplexMQVersion
loadSavedFingerprint :: IO String
loadSavedFingerprint = withFile fingerprintFile ReadMode hGetLine
+6
View File
@@ -0,0 +1,6 @@
packages: .
source-repository-package
type: git
location: git://github.com/simplex-chat/aeson.git
tag: 3eb66f9a68f103b5f1489382aad89f5712a64db7
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 506.94 131.5"><path d="m109.65 58.21-18.21-10.07-15.37 9.38-.19 9.59-7.47-4.92-10.16 6.19-.44-10.48-10.48-7 10-5.17c-.08 0 0 1-1.48-34.34l-23.65-11.39-32.2 10 7.19 34.63 10.81 8.37-8.22 3.9 5.37 26.1 7.55 7.09-5.4 3.29 4.19 20.18 16.94 17.94c.08-.1 1.78-1.41 21.79-17.32l-.58-13.71 8.58 7.28c.12-.12 1.84-1.39 17.56-13.89l.61-10.14 6.48 4.5c.13-.12 1.58-1.22 14.26-11.33z" fill="#231f20"/><path d="m94.54 69 15.11-10.79-18.21-10.07-15.37 9.38z"/><path d="m92.87 88.2 1.67-19.2-18.47-11.48-.38 18.75z" fill="#004b16"/><path d="m68.22 107.73v-19.89l-18.66-14.15 1.47 19.54z" fill="#004b16"/><path d="m68.22 87.84 18.74-13.43-18.55-12.22-18.85 11.5z"/><path d="m38.43 131.48-2.98-20.32-18.15-17.8 4.19 20.18z" fill="#004b16"/><path d="m35.45 111.16 23.91-17.06-18.53-15.09-23.53 14.35z"/><path d="m33.9 100.6-3.94-26.88-20.22-16.81 5.41 26.07z" fill="#004b16"/><path d="m29.96 73.72 27.85-15.82-20.8-13.94-27.27 12.95z"/><path d="m28.07 60.88-5.4-36.81-22.67-14.06 7.19 34.62z" fill="#004b16"/><path d="m22.67 24.07 33.14-12.7-23.61-11.37-32.2 10.01z"/><g fill="#1cb35c"><path d="m107.13 76.87c-14.02 11.13-14.33 11.33-14.26 11.33 1.74-20.1 1.59-19.2 1.67-19.2 16-11.45 15-10.79 15.11-10.79z"/><path d="m85.78 93.84c-17.35 13.8-17.63 13.89-17.56 13.89-.17-20.82-.07-19.89 0-19.89 20-14.3 18.67-13.43 18.74-13.43z"/><path d="m60.22 114.16c-21.66 17.22-21.86 17.32-21.79 17.32-3.07-20.94-3-20.32-3-20.32 25.47-18.16 23.86-17.06 23.93-17.06z"/><path d="m55.81 11.37c1.52 35.37 1.4 34.34 1.48 34.34-28.66 14.89-29.29 15.17-29.22 15.17-5.52-37.63-5.47-36.81-5.4-36.81z"/><path d="m57.81 57.9c1.15 26.81 1 25.88 1.11 25.88-24.81 16.67-25.09 16.82-25 16.82-4-27.58-4-26.88-3.94-26.88z"/></g><path xmlns="http://www.w3.org/2000/svg" d="m151.61 14.24 16.58-4v79.88q0 13.13 7.83 15.65-3.84 7.31-13.13 7.3-11.28 0-11.28-15.66z"/><path d="m186.66 111.72v-57.42h-9.08v-13.6h25.86v71zm8.56-98.54a9.62 9.62 0 1 1 -9.62 9.62 9.63 9.63 0 0 1 9.62-9.62z"/><path d="m262.94 111.74v-41.06c0-6.05-1.16-10.48-3.48-13.26s-6.11-4.18-11.37-4.18a17.74 17.74 0 0 0 -7.8 2.06 18 18 0 0 0 -6.46 5.1v51.34h-16.59v-71h11.94l3 6.64q6.76-8 20-8 12.66 0 20 7.59t7.33 21.19v43.58z"/><path d="m288.42 76.06q0-16.26 9.38-26.47t24.77-10.21q16.19 0 25.14 9.82t9 26.86q0 17-9.12 27t-25 10q-16.18 0-25.17-10.12t-9-26.88zm17.24 0q0 23.47 16.91 23.48a14.54 14.54 0 0 0 12.31-6.11q4.55-6.09 4.54-17.37 0-23.14-16.85-23.15a14.61 14.61 0 0 0 -12.33 6.09q-4.58 6.11-4.58 17.06z"/><path d="m412.37 111.74v-4.31c-1.37 1.5-3.71 2.82-7 3.95a31.33 31.33 0 0 1 -10.15 1.69q-14.87 0-23.38-9.42t-8.52-26.27q0-16.84 9.78-27.42a32 32 0 0 1 24.51-10.58 32.37 32.37 0 0 1 14.72 3.32v-28.46l16.58-4v101.5zm0-54.06a17.6 17.6 0 0 0 -11.07-4.24q-10 0-15.33 6.07t-5.37 17.41q0 22.15 21.36 22.15a16.11 16.11 0 0 0 5.87-1.42c2.32-1 3.83-1.92 4.54-2.89z"/><path d="m505.55 81.3h-50.74q.46 8.49 5.83 13.2t14.46 4.7q11.34 0 17.25-5.9l6.43 12.7q-8.75 7.09-26.13 7.1-16.25 0-25.7-9.52t-9.45-26.58q0-16.78 10.38-27.2a33.91 33.91 0 0 1 24.9-10.41q15.45 0 24.81 9.22t9.35 23.48a46.28 46.28 0 0 1 -1.39 9.21zm-50.15-12.47h34.89q-1.73-15.58-17.24-15.59-14.2 0-17.65 15.59z"/></svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

+19 -10
View File
@@ -1,5 +1,5 @@
name: simplexmq
version: 0.3.2
version: 1.0.3
synopsis: SimpleXMQ message broker
description: |
This package includes <./docs/Simplex-Messaging-Server.html server>,
@@ -15,44 +15,54 @@ homepage: https://github.com/simplex-chat/simplexmq#readme
license: AGPL-3
author: simplex.chat
maintainer: chat@simplex.chat
copyright: 2020 simplex.chat
copyright: 2020-2022 simplex.chat
category: Chat, Network, Web, System, Cryptography
extra-source-files:
- README.md
- CHANGELOG.md
dependencies:
- ansi-terminal == 0.10.*
- aeson == 2.0.*
- ansi-terminal >= 0.10 && < 0.12
- asn1-encoding == 0.9.*
- asn1-types == 0.3.*
- async == 2.2.*
- attoparsec == 0.13.*
- attoparsec == 0.14.*
- base >= 4.7 && < 5
- base64-bytestring >= 1.0 && < 1.3
- bytestring == 0.10.*
- composition == 1.0.*
- constraints >= 0.12 && < 0.14
- containers == 0.6.*
- cryptonite == 0.27.*
- cryptonite >= 0.27 && < 0.30
- cryptostore == 0.2.*
- data-default == 0.7.*
- direct-sqlite == 2.3.*
- directory == 1.3.*
- filepath == 1.4.*
- generic-random == 1.3.*
- http-types == 0.12.*
- generic-random >= 1.3 && < 1.5
- iso8601-time == 0.1.*
- memory == 0.15.*
- mtl == 2.2.*
- network == 3.1.*
- network-transport == 0.5.*
- QuickCheck == 2.14.*
- random == 1.1.*
- random >= 1.1 && < 1.3
- simple-logger == 0.1.*
- sqlite-simple == 0.4.*
- stm == 2.5.*
- template-haskell == 2.16.*
- text == 1.2.*
- time == 1.9.*
- tls >= 1.5.7 && < 1.6
- transformers == 0.5.*
- unliftio == 0.2.*
- unliftio-core == 0.2.*
- websockets == 0.12.*
- x509 == 1.7.*
- x509-store == 1.6.*
- x509-validation == 1.6.*
library:
source-dirs: src
@@ -62,9 +72,9 @@ executables:
source-dirs: apps/smp-server
main: Main.hs
dependencies:
- cryptostore == 0.2.*
- ini == 0.4.*
- optparse-applicative == 0.15.*
- optparse-applicative >= 0.15 && < 0.17
- process == 1.6.*
- simplexmq
ghc-options:
- -threaded
@@ -86,7 +96,6 @@ tests:
- hspec == 2.7.*
- hspec-core == 2.7.*
- HUnit == 1.6.*
- random == 1.1.*
- QuickCheck == 2.14.*
- timeit == 2.0.*
+200 -95
View File
@@ -12,79 +12,86 @@
- [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)
- [JOIN command](#join-command)
- [CON notification](#con-notification)
- [CONF notification and LET command](#conf-notification-and-let-command)
- [REQ notification and ACPT command](#req-notification-and-acpt-command)
- [INFO and CON notifications](#info-and-con-notifications)
- [SUB command](#sub-command)
- [SEND command and SENT response](#send-command-and-sent-response)
- [SEND command and MID, SENT and MERR responses](#send-command-and-mid-sent-and-merr-responses)
- [MSG notification](#msg-notification)
- [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 queue 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.
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 unauthenticated message to SMP queue with ephemeral key that will be used to authenticate commands to the queue, as described in SMP protocol.
- Agent A receives the KEY and secures the queue.
- 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 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 commands to the queue, as described in SMP protocol.
- Bob's agent receives the key 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.
- Once sending `HELLO` succeeds, Alice's agent sends to Alice `CON` notification that confirms that now both parties can communicate.
@@ -92,17 +99,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:
@@ -110,8 +121,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. The only case the message padding can be absent is when the message has exactly the maximum size, in all other cases the message MUST be padded to a fixed size.
### Messages between SMP agents
@@ -119,62 +131,69 @@ Message syntax below uses [ABNF][3] with [case-sensitive strings extension][4].
```abnf
decryptedSmpMessageBody = agentMsgHeader CRLF agentMessage CRLF msgPadding
agentMsgHeader = agentMsgId SP agentTimestamp SP previousMsgHash ; here `agentMsgId` is sequential ID set by the sending agent
agentMsgHeader = agentMsgId SP previousMsgHash ; here `agentMsgId` is sequential ID set by the sending agent
agentMsgId = 1*DIGIT
agentTimestamp = <date-time> ; RFC3339
previousMsgHash = encoded
encoded = <base64 encoded>
agentMessage = helloMsg / replyQueueMsg / deleteQueueMsg / clientMsg / acknowledgeMsg
agentMessage = helloMsg / replyQueueMsg /
clientMsg / invitationMsg /
newQueueMessage / deleteQueueMsg
msgPadding = *OCTET ; optional random bytes to get messages to the same size (as defined in SMP message size)
helloMsg = %s"HELLO" 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
helloMsg = %s"H"
replyQueueMsg = %s"REPLY" SP <queueInfo> ; `queueInfo` is the same as in out-of-band message, see SMP protocol
replyQueueMsg = %s"R" 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
clientMsg = %s"M" clientMsgBody
clientMsgBody = *OCTET
acknowledgeMsg = %s"ACK" SP agentMsgId SP ackStatus
; NOT SUPPORTED with the current implementation
; TODO remove and move to "public" header
invitationMsg = %s"INV" SP connReqInvitation SP connInfo
; `connReqInvitation` and `connInfo` are defined below
ackStatus = %s"OK" / ackError
newQueueMsg = %s"N" queueURI
; this message can be sent by any party to add SMP queue to the connection.
; NOT SUPPORTED in the current implementation
ackError = %s"ERR" SP ackErrorType
ackErrorType = ackUnknownMsg / ackProhibitedMsg / ackSyntaxErr
ackUnknownMsg = %s"UNKNOWN"
ackProhibitedMsg = %s"PROHIBITED" ; e.g. "HELLO" or "REPLY"
ackSyntaxErr = %s"SYNTAX" SP syntaxErrCode
syntaxErrCode = 1*DIGIT ; TODO
deleteQueueMsg = %s"D" queueURI
; notification that the queue with passed URI will be deleted
; no need to notify the other party about suspending queue separately, as suspended and deleted queues are indistinguishable to the sender
; NOT SUPPORTED in the current implementation
```
#### 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.
#### 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.
#### 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 last remaining send queue in the duplex connection, the agent 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.
@@ -184,21 +203,41 @@ Commands syntax below is provided using [ABNF][3] with [case-sensitive strings e
Each transmission between the user and SMP agent must have this format/syntax:
```abnf
agentTransmission = [corrId] CRLF [cAlias] CRLF agentCommand
agentTransmission = [corrId] CRLF [connId] CRLF agentCommand
corrId = 1*(%x21-7F) ; any characters other than control/whitespace
cAlias = cId / cName
cId = encoded
cName = 1*(ALPHA / DIGIT / "_" / "-")
connId = encoded
agentCommand = (userCmd / agentMsg) CRLF
userCmd = newCmd / joinCmd / subscribeCmd / sendCmd / acknowledgeCmd / suspendCmd / deleteCmd
agentMsg = invitation / connected / unsubscribed / message / sent / received / ok / error
userCmd = newCmd / joinCmd / letCmd / acceptCmd / subscribeCmd / sendCmd / acknowledgeCmd / suspendCmd / deleteCmd
agentMsg = invitation / confMsg / connReqMsg / connInfo / connected / unsubscribed / connDown / connUp / messageId / sent / messageError / message / received / ok / error
newCmd = %s"NEW" [SP %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 <queueInfo> ; `queueInfo` is the same as in out-of-band message, see SMP protocol
connectionMode = %s"INV" / %s"CON"
invitation = %s"INV" SP connectionRequest ; `connectionRequest` is defined below
confMsg = %s"CONF" SP confirmationId SP msgBody
; msgBody here is any binary information identifying connection request
letCmd = %s"LET" SP confirmationId SP msgBody
; msgBody here is any binary information identifying connecting party
confirmationId = 1*DIGIT
connReqMsg = %s"REQ" SP invitationId SP msgBody
; msgBody here is any binary information identifying connection request
acceptCmd = %s"ACPT" SP invitationId SP msgBody
; msgBody here is any binary information identifying connecting party
invitationId = 1*DIGIT
connInfo = %s"INFO" SP msgBody
; msgBody here is any binary information identifying connecting party
connected = %s"CON"
@@ -208,9 +247,16 @@ unsubscribed = %s"END"
; when another agent (or another client of the same agent)
; subscribes to the same SMP queue on the server
joinCmd = %s"JOIN" SP <queueInfo> [SP %s"NO_REPLY"] [SP %s"NO_ACK"]
; `queueInfo` is the same as in out-of-band message, see SMP protocol
connDown = %s"DOWN"
; lost connection (e.g. because of Internet connectivity or server is down)
connUp = %s"UP"
; restored connection
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`
@@ -225,18 +271,22 @@ binaryMsg = size CRLF msgBody CRLF ; the last CRLF is in addition to CRLF in the
size = 1*DIGIT ; size in bytes
msgBody = *OCTET ; any content of specified size - safe for binary
messageId = %s"MID" SP agentMsgId
sent = %s"SENT" SP agentMsgId
messageError = %s"MERR" SP agentMsgId SP <errorType>
message = %s"MSG" SP msgIntegrity SP recipientMeta SP brokerMeta SP senderMeta SP binaryMsg
recipientMeta = %s"R=" agentMsgId "," agentTimestamp ; receiving agent message metadata
brokerMeta = %s"B=" brokerMsgId "," brokerTimestamp ; broker (server) message metadata
senderMeta = %s"S=" agentMsgId "," agentTimestamp ; sending agent message metadata
senderMeta = %s"S=" agentMsgId ; sending agent message ID
brokerMsgId = encoded
brokerTimestamp = <date-time>
msgIntegrity = ok / messageError
msgIntegrity = ok / msgIntegrityError
messageError = %s"ERR" SP messageErrorType
messageErrorType = skippedMsgErr / badMsgIdErr / badHashErr
msgIntegrityError = %s"ERR" SP msgIntegrityErrorType
msgIntegrityErrorType = skippedMsgErr / badMsgIdErr / badHashErr
skippedMsgErr = %s"NO_ID" SP missingFromMsgId SP missingToMsgId
badMsgIdErr = %s"ID" SP previousMsgId ; ID is lower than the previous
@@ -247,10 +297,12 @@ missingToMsgId = agentMsgId
previousMsgId = agentMsgId
acknowledgeCmd = %s"ACK" SP agentMsgId ; ID assigned by receiving agent (in MSG "R")
received = %s"RCVD" SP agentMsgId SP msgIntegrity
; ID assigned by sending agent (in SENT response)
; currently not implemented
received = %s"RCVD" SP agentMsgId ; ID assigned by sending agent (in SENT response)
; currently not implemented
msgStatus = ok | error
ok = %s"OK"
@@ -261,27 +313,49 @@ error = %s"ERR" SP <errorType>
#### 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. It should be used by the client of the agent that initiates creating a duplex connection.
`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.
`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 invitation received out-of-band. It should be used by the client of the agent that accepts the connection.
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).
#### CON notification
#### CONF notification and LET command
It is sent by both agents managing duplex connection to their clients once the connection is established and ready to accept client messages.
When the joining party uses `JOIN` command to accept connection invitation created with `NEW INV` command, the initiating party will receive `CONF` notification with some numeric identifier and an additional binary information, that can be used to identify the joining party or for any other purpose.
To continue with the connection the initiating party should use `LET` command.
#### REQ notification and ACPT command
When the joining party uses `JOIN` command to connect to the contact created with `NEW CON` command, the initiating party will receive `REQ` notification with some numeric identifier and an additional binary information, that can be used to identify the joining party or for any other purpose.
To continue with the connection the party that created the contact should use `ACPT` command.
#### INFO and CON notifications
After the initiating party proceeds with the connection using `ACPT` command, the joining party will receive `INFO` notification that can be used to identify the initiating party or for any other purpose.
Once the connection is established and ready to accept client messages, both agents will send `CON` notification to their clients.
#### SUB command
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 SENT response
#### SEND command and MID, SENT, RCVD and MERR responses
`SEND` command is used to the client to send messages
`SEND` command is used by the client to send messages.
`SENT` response is sent by the agent to confirm that the message was delivered to the SMP server. Message ID in this response is the sequential message number that includes both sent and received messages in the connection.
`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` 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.
`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 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
@@ -294,6 +368,12 @@ It is sent by the agent to the client when agent receives the message from the S
It is sent by the agent to the client when agent receives SMP protocol `END` notification from SMP server. It indicates that another agent has subscribed to the same SMP queue on the server and the server terminated the subscription of the current agent.
#### DOWN and UP notifications
These notifications are sent when server or network connection is, respectively, `DOWN` or back `UP`.
All the subscriptions made in the current client session will be automatically resumed when `UP` notification is received.
#### OFF command
It is used to suspend the receiving SMP queue - sender will no longer be able to send the messages to the connection, but the recipient can retrieve the remaining messages. Agent response to this command can be `OK` or `ERR`. This command is irreversible.
@@ -302,11 +382,36 @@ 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 = 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
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 = <base64url X509 SPKI key encoding>
smpQueues = smpQueue [ "," 1*smpQueue ] ; SMP queues for the connection
smpQueue = <URL-encoded queueURI defined in SMP protocol>
```
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
@@ -27,11 +27,14 @@ sequenceDiagram
note over BA: status: NONE/NEW
note over BA, AA: 5. establish Alice's SMP queue
BA ->> AS: SEND: KEY: sender's server key
BA ->> AS: SEND: Bob's info and sender server key (SMP confirmation)
note over BA: status: NONE/CONFIRMED
activate BA
AS ->> AA: MSG: KEY: sender's server key
AS ->> AA: MSG: Bob's info and<br>sender server key
note over AA: status: CONFIRMED/NONE
AA ->> AS: ACK: confirm message
AA ->> A: REQ: connection request ID<br>and Bob's info
A ->> AA: ACPT: accept connection request,<br>send Alice's info
AA ->> AS: KEY: secure queue
note over AA: status: SECURED/NONE
@@ -40,6 +43,7 @@ sequenceDiagram
note over BA: status: NONE/ACTIVE
AS ->> AA: MSG: HELLO: Alice's agent<br>knows Bob can send
note over AA: status: ACTIVE/NONE
AA ->> AS: ACK: confirm message
note over BA, BS: 6. create Bob's SMP queue
BA ->> BS: NEW: create SMP queue
@@ -51,12 +55,15 @@ sequenceDiagram
note over BA: status: PENDING/ACTIVE
AS ->> AA: MSG: REPLY: invitation<br>to connect
note over AA: status: ACTIVE/NEW
AA ->> AS: ACK: confirm message
AA ->> BS: SEND: KEY: sender's server key
AA ->> BS: SEND: Alice's info and sender's server key
note over AA: status: ACTIVE/CONFIRMED
activate AA
BS ->> BA: MSG: KEY: sender's server key
BS ->> BA: MSG: Alice's info and<br>sender's server key
note over BA: status: CONFIRMED/ACTIVE
BA ->> B: INFO: Alice's info
BA ->> BS: ACK: confirm message
BA ->> BS: KEY: secure queue
note over BA: status: SECURED/ACTIVE
@@ -65,6 +72,7 @@ sequenceDiagram
note over AA: status: ACTIVE/ACTIVE
BS ->> BA: MSG: HELLO: Bob's agent<br>knows Alice can send
note over BA: status: ACTIVE/ACTIVE
BA ->> BS: ACK: confirm message
note over A, B: 8. notify users about connection success
AA ->> A: CON: connected
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 35 KiB

+313
View File
@@ -0,0 +1,313 @@
Revision 1, 2022-01-01
Evgeny Poberezkin
# SimpleX: messaging and application platform
## Table of contents
- [Introduction](#introduction)
- [What is SimpleX](#what-is-simplex)
- [SimpleX objectives](#simplex-objectives)
- [In Comparison](#in-comparison)
- [Technical Details](#technical-details)
- [Trust in Servers](#trust-in-servers)
- [Client -> Server Communication](#client---server-communication)
- [SimpleX Messaging Protocol](#simplex-messaging-protocol)
- [SimpleX Agents](#simplex-agents)
- [Encryption Primitives Used](#encryption-primitives-used)
- [Threat model](#threat-model)
- [Acknowledgements](#acknowledgements)
## Introduction
#### What is SimpleX
SimpleX as a whole is a platform upon which applications can be built. [SimpleX Chat](https://github.com/simplex-chat/simplex-chat) is one such application that also serves as an example and reference application.
- [SimpleX Messaging Protocol](https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md) (SMP) is a protocol to send messages in one direction to a recipient, relying on a server in-between. The messages are delivered via uni-directional queues created by recipients.
- SMP runs over a transport protocol (shown below as TLS) that provides integrity, server authentication, confidentiality, and transport channel binding.
- A SimpleX Server is one of those servers.
- The SimpleX Network is the term used for the collective of SimpleX Servers that facilitate SMP.
- SimpleX Client libraries speak SMP to SimpleX Servers and provide a low-level API not generally intended to be used by applications.
- SimpleX Agents interface with SimpleX Clients to provide a more high-level API intended to be used by applications. Typically they are embedded as libraries, but are designed so they can also be abstracted into local services.
*Diagram showing the SimpleX Chat app, with logical layers of the chat application interfacing with a SimpleX Agent library, which in turn interfaces with a SimpleX Client library. The Client library in turn speaks the Messaging Protocol to a SimpleX Server.*
```
User's Computer Internet Third-Party Server
------------------ | ---------------------- | -------------------------
| |
SimpleX Chat | |
| |
+----------------+ | |
| Chat App | | |
+----------------+ | |
| SimpleX Agent | | |
+----------------+ -------------- TLS ---------------- +----------------+
| SimpleX Client | ------ SimpleX Messaging Protocol ------> | SimpleX Server |
+----------------+ ----------------------------------- +----------------+
| |
```
#### SimpleX objectives
1. Provide messaging infrastructure for distributed applications. This infrastructure needs to have the following qualities:
- Security against passive and active (man-in-the-middle) attacks: the parties should have reliable end-to-end encryption and be able to detect the presence of an active attacker who modified, deleted or added messages.
- Privacy: protect against traffic correlation attacks to determine the contacts that the users communicate with.
- Reliability: the messages should be delivered even if some participating network servers or receiving clients fail, with “at least once” delivery guarantee.
- Integrity: the messages sent in one direction are ordered in a way that sender and recipient agree on; the recipient can detect when a message was removed or changed.
- Asynchronous delivery: it should not be required that both communicating parties (client devices, services or applications) are online for reliable message delivery.
- Low latency: the delay introduced by the network should not be higher than 100ms-1s in addition to the underlying TCP network latency.
2. Provide better communication security and privacy than the alternative instant messaging solutions. In particular SimpleX provides better privacy of metadata (who talks to whom and when) and better security against active network attackers and malicious servers.
3. Balance user experience with privacy requirements, prioritizing experience of mobile device users.
#### In Comparison
SimpleX network has a design similar to P2P networks, but unlike most P2P networks it consists of clients and servers without depending on any centralized component.
In comparison to more traditional messaging applications (e.g. WhatsApp, Signal, Telegram) the key differences of SimpleX network are:
- participants do not need to have globally unique addresses to communicate, instead they use redundant unidirectional (simplex) messaging queues, with a separate set of queues for each contact.
- connection requests are passed out-of-band, non-optionally protecting key exchange against man-in-the-middle attack.
- simple message queues provided by network servers are used by the clients to create more complex communication scenarios, such as duplex one-to-one communication, transmitting files, group communication without central servers, and content/communication channels.
- servers do not store any user information (no user profiles or contacts, or messages once they are delivered), and primarily use in-memory persistence.
- users can change servers with minimal disruption - even after an in-use server disappears, simply by changing the configuration on which servers the new queues are created.
## Technical Details
#### Trust in Servers
Clients communicate directly with servers (but not with other clients) using SimpleX Messaging Protocol (SMP) running over some transport protocol that provides integrity, server authentication, confidentiality, and transport channel binding. By default, we assume this transport protocol is TLS.
Users use multiple servers, and choose where to receive their messages. Accordingly, they send messages to their communication partners' chosen servers.
Although end-to-end encryption is always present, users place a degree of trust in servers. This trust decision is very similar to a user's choice of email provider; however the trust placed in a SimpleX server is significantly less. Notably, there is no re-used identifier or credential between queues on the same (or different) servers. While a user *may* re-use a connection to fetch from multiple queues, or connect to a server from the same IP address, both are choices a user may opt into to break the promise of un-correlatable queues.
Users may trust a server because:
- They deploy and control the servers themselves from the available open-source code. This has the trade-offs of strong trust in the server but limited metadata obfuscation to a passive network observer. Techniques such as noise traffic, traffic mixing (incurring latency), and using an onion routing transport protocol can mitigate that latter.
- They use servers from a trusted commercial provider. The more clients the provider has, the less metadata about the communication times is leaked to the network observers.
- Users trust their contacts and the servers they chose.
By default, servers do not retain access logs, and permanently delete messages and queues when requested. Messages persist only in memory until they cross a threshold of time, typically on the order of days.[0] There is still a risk that a server maliciously records all queues and messages (even though encrypted) sent via the same transport connection to gain a partial knowledge of the users communications graph and other meta-data.
SimpleX supports measures (managed transparently to the user at the agent level) to mitigate the trust placed in servers. These include rotating the queues in use between users, noise traffic, and supporting overlay networks such as Tor.
[0] While configurable by servers, a minimum value is enforced by the default software. SimpleX Agents provide redundant routing over queues to mitigate against message loss.
#### Client -> Server Communication
Utilizing TLS grants the SimpleX Messaging Protocol (SMP) server authentication and metadata protection to a passive network observer. But SMP does not rely on the transport protocol for message confidentiality or client authentication. The SMP protocol itself provides end-to-end confidentiality, authentication, and integrity of messages between communicating parties.
Servers have long-lived, self-signed, offline certificates whose hash is pre-shared with clients over secure channels - either provided with the client library or provided in the secure introduction between clients. The offline certificate signs an online certificate used in the transport protocol handshake. [0]
If the transport protocol's confidentiality is broken, incoming and outgoing messages to the server cannot be correlated by message contents. Additionally, because of encryption at the SMP layer, impersonating the server is not sufficient to pass (and therefore correlate) a message from a sender to recipient - the only attack possible is to drop the messages. Only by additionally *compromising* the server can one pass and correlate messages.
It's important to note that the SMP protocol does not do server authentication. Instead we rely upon the fact that an attacker who tricks the transport protocol into authenticating the server incorrectly cannot do anything with the SMP messages except drop them.
After the connection is established, the client sends blocks of a fixed size 16Kb, and the server replies with the blocks of the same size to reduce metadata observable to a network adversary. The protocol has been designed to make traffic correlation attacks difficult, adapting ideas from Tor, remailers, and more general onion and mix networks. It does not try to replace Tor though - SimpleX servers can be deployed as onion services and SimpleX clients can communicate with servers over Tor to further improve participants privacy.
By using fixed-size blocks, oversized for the expected content, the vast majority of traffic is uniform in nature. When enough traffic is transiting a server simultaneously, the server acts as a (very) low-latency mix node. We can't rely on this behavior to make a security claim, but we have engineered to take advantage of it when we can. As mentioned, this holds true even if the transport connection is compromised.
The protocol does not protect against attacks targeted at particular users with known identities - e.g., if the attacker wants to prove that two known users are communicating, they can achieve it. At the same time, it substantially complicates large-scale traffic correlation, making determining the real user identities much less effective.
[0] Future versions of SMP may add support for revocation lists of certificates, presently this risk is mitigated by the SMP protocol itself.
#### SimpleX Messaging Protocol
SMP is initialized with an in-person or out-of-band introduction message, where Alice provides Bob with details of a server (including IP, port, and hash of the long-lived offline certificate), a queue ID, and Alice's public key for her receiving queue. These introductions are similar to the PANDA key-exchange, in that if observed, the adversary can race to establish the communication channel instead of the intended participant. [0]
Because queues are uni-directional, Bob provides an identically-formatted introduction message to Alice over Alice's now-established receiving queue.
When setting up a queue, the server will create separate sender and recipient queue IDs (provided to Alice during set-up and Bob during initial connection). Additionally, during set-up Alice will perform a DH exchange with the server to agree upon a shared secret. This secret will be used to re-encrypt Bob's incoming message before Alice receives it, creating the anti-correlation property earlier-described should the transport encryption be compromised.
[0] Users can additionally create public 'contact queues' that are only used to receive connection requests.
#### SimpleX Agents
SimpleX agents provide higher-level operations compared to SimpleX Clients, who are primarily concerned with creating queues and communicating with servers using SMP. Agent operations include:
- Managing sets of bi-directional, redundant queues for communication partners
- Providing end-to-end encryption of messages
- Rotating queues periodically with communication partners
- Noise traffic
#### Encryption Primitives Used
- Ed448 to sign/verify commands to SMP servers (Ed25519 is also supported via client/server configuration).
- Curve25519 for DH exchange to agree:
- the shared secret between server and recipient (to encrypt message bodies - it avoids shared cipher-text in sender and recipient traffic)
- the shared secret between sender and recipient (to encrypt messages end-to-end in each queue - it avoids shared cipher-text in redundant queues).
- [NaCl crypto_box](https://nacl.cr.yp.to/box.html) encryption scheme (curve25519xsalsa20poly1305) for message body encryption between server and recipient and for E2E per-queue encryption.
- SHA256 to validate server offline certificates.
- [double ratchet](https://signal.org/docs/specifications/doubleratchet/) protocol for end-to-end message encryption between the agents:
- Curve448 keys to agree shared secrets required for double ratchet initialization (using [X3DH](https://signal.org/docs/specifications/x3dh/) key agreement with 2 ephemeral keys for each side),
- AES-GCM AEAD cipher,
- SHA512-based HKDF for key derivation.
## Threat Model
#### Global Assumptions
- A user protects their local database and key material
- The user's application is authentic, and no local malware is running
- The cryptographic primitives in use are not broken
- A user's choice of servers is not directly tied to their identity or otherwise represents distinguishing information about the user.
#### A passive adversary able to monitor the traffic of one user
*can:*
- identify that and when a user is using SimpleX
- block SimpleX traffic
- determine which servers the user communicates with
- observe how much traffic is being sent, and make guesses as to its purpose.
*cannot:*
- see who sends messages to the user and who the user sends the messages to
#### A passive adversary able to monitor a set of senders and recipients
*can:*
- identify who and when is using SimpleX
- learn which SimpleX Messaging Protocol servers are used as receive queues for which users
- learn when messages are sent and received
- perform traffic correlation attacks against senders and recipients and correlate senders and recipients within the monitored set, frustrated by the number of users on the servers
- observe how much traffic is being sent, and make guesses as to its purpose
*cannot, even in case of a compromised transport protocol:*
- perform traffic correlation attacks with any increase in efficiency over a non-compromised transport protocol
#### SimpleX Messaging Protocol server
*can:*
- learn when a queue recipient or sender is online
- know how many messages are sent via the queue (although some may be noise)
- perform queue correlation (matching multiple queues to a single user) via either a re-used transport connection, user's IP Address, or connection timing regularities
- learn a user's IP address, track them through other IP addresses they use to access the same queue, and infer information (e.g. employer) based on the IP addresses, as long as Tor is not used.
- drop all future messages inserted into a queue, detectable only over other, redundant queues
- lie about the state of a queue to the recipient and/or to the sender (e.g. suspended or deleted when it is not).
- spam a user with invalid messages
*cannot:*
- undetectably add, duplicate, or corrupt individual messages
- undetectably drop individual messages, so long as a subsequent message is delivered
- learn the contents of messages
- distinguish noise messages from regular messages except via timing regularities
- compromise the user's end-to-end encryption with an active attack
#### An attacker who obtained Alice's (decrypted) chat database
*can:*
- see the history of all messages exchanged by Alice with her communication partners
- see shared profiles of contacts and groups
- surreptitiously receive new messages sent to Alice via existing queues; until communication queues are rotated or the Double-Ratchet advances forward
- prevent Alice from receiving all new messages sent to her - either surreptitiously by emptying the queues regularly or overtly by deleting them
- send messages from the user to their contacts; recipients will detect it as soon as the user sends the next message, because the previous message hash wont match (and potentially wont be able to decrypt them in case they dont keep the previous ratchet keys).
*cannot:*
- impersonate a sender and send messages to the user whose database was stolen. Doing so requires also compromising the server (to place the message in the queue, that is possible until the Double-Ratchet advances forward) or the user's device at a subsequent time (to place the message in the database).
- undetectably communicate at the same time as Alice with her contacts. Doing so would result in the contact getting different messages with repeated IDs.
- undetectably monitor message queues in realtime without alerting the user they are doing so, as a second subscription request unsubscribes the first and notifies the second.
#### A users contact
*can:*
- spam the user with messages
- forever retain messages from the user
*cannot:*
- cryptographically prove to a third-party that a message came from a user (assuming the users device is not seized)
- prove that two contacts they have is the same user
- cannot collaborate with another of the user's contacts to confirm they are communicating with the same user
#### An attacker who observes Alice showing an introduction message to Bob
*can:*
- Impersonate Bob to Alice
*cannot:*
- Impersonate Alice to Bob
#### An attacker with Internet access
*can:*
- Denial of Service SimpleX messaging servers
- spam a user's public “contact queue” with connection requests
*cannot:*
- send messages to a user who they are not connected with
- enumerate queues on a SimpleX server
## Acknowledgements
Efim Poberezkin contributed to the design and implementation of [SimpleX Messaging Protocol](https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md) and [SimpleX Agent Protocol](https://github.com/simplex-chat/simplexmq/blob/master/protocol/agent-protocol.md) since 2019.
Adam Langley's [Pond](https://github.com/agl/pond) inspired some of the recent improvements and the structure of this document.
+355 -125
View File
@@ -7,11 +7,14 @@
- [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 Transmission structure](#smp-transmission-structure)
- [SMP commands](#smp-commands)
- [Correlating responses with commands](#correlating-responses-with-commands)
- [Command authentication](#command-authentication)
@@ -20,14 +23,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 +74,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 = <hostname> ; 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 = <base64 X509 key encoding>
encoded = <base64 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
@@ -104,7 +90,7 @@ The simplex queue is the main unit of SMP protocol. It is used by:
- Sender of the queue (who received out-of-band message) to send messages to the server using sender's queue ID, signed by sender's key.
- Recipient of the queue (who created the queue and sent out-of-band message) will use it to retrieve messages from the server, signing the commands by the recipient key.
- Recipient of the queue (who created the queue and sent out-of-band message) will use it to retrieve messages from the server, signing the commands by the recipient key. Recipient decrypts the messages with the key negotiated during the creation of the queue.
- Participant identities are not shared with the server - new unique keys and queue IDs are used for each queue.
@@ -114,7 +100,7 @@ This approach is based on the concept of [unidirectional networks][4] that are u
Access to each queue is controlled with unique (not shared with other queues) asymmetric key pairs, separate for the sender and the recipient. The sender and the receiver have private keys, and the server has associated public keys to authenticate participants' commands by verifying cryptographic signatures.
The messages sent over the queue are encrypted and decrypted using another key pair that was shared via out-of-band message - the recipient has the private key and the sender has the associated public key.
The messages sent over the queue are end-to-end encrypted using the DH secret agreed via out-of-band message and SMP confirmation.
**Simplex queue diagram:**
@@ -124,6 +110,34 @@ 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 "#" recipientDhPublicKey
smpServer = serverIdentity "@" srvHost [":" port]
srvHost = <hostname> ; RFC1123, RFC5891
port = 1*DIGIT
serverIdentity = base64url
queueId = base64url
base64url = <base64url encoded binary> ; RFC4648, section 5
recipientDhPublicKey = x509UrlEncoded
; the recipient's Curve25519 key for DH exchange to derive the secret
; that the sender will use to encrypt delivered messages
; using [NaCl crypto_box][16] encryption scheme (curve25519xsalsa20poly1305).
x509UrlEncoded = <base64url X509 key encoding>
```
`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).
@@ -138,17 +152,21 @@ To create and start using a simplex queue Alice and Bob follow these steps:
3. Generates another new random public/private key pair (recipient key - `RK`) that she did not use before for her to sign commands and to decrypt the transmissions received from the server.
4. Sends `"NEW"` command to the server to create a simplex queue (see `create` in [Create queue command](#create-queue-command)). This command contains previously generated unique "public" key `RK` that will be used to verify the following commands related to the same queue signed by its private counterpart, for example to subscribe to the messages received to this queue or to update the queue, e.g. by setting the key required to send the messages (initially Alice creates the queue that accepts unsigned messages, so anybody could send the message via this queue if they knew the queue sender's ID and server address).
4. Generates one more random key pair (recipient DH key - `RDHK`) to negotiate symmetric key that will be used by the server to encrypt message bodies delivered to Alice (to avoid shared cipher-text inside transport connection).
5. The server sends `"IDS"` response with queue IDs (`queueIds`):
5. Sends `"NEW"` command to the server to create a simplex queue (see `create` in [Create queue command](#create-queue-command)). This command contains previously generated unique "public" keys `RK` and `RDHK`. `RK` will be used to verify the following commands related to the same queue signed by its private counterpart, for example to subscribe to the messages received to this queue or to update the queue, e.g. by setting the key required to send the messages (initially Alice creates the queue that accepts unsigned messages, so anybody could send the message via this queue if they knew the queue sender's ID and server address).
6. The server sends `"IDS"` response with queue IDs (`queueIds`):
- Recipient ID `RID` for Alice to manage the queue and to receive the messages.
- Sender ID `SID` for Bob to send messages to the queue.
- Server public DH key (`SDHK`) to negotiate a shared secret for message body encryption, that Alice uses to derive a shared secret with the server `SS`.
2. Alice sends an out-of-band message to Bob via the alternative channel that both Alice and Bob trust (see [protocol abstract](#simplex-messaging-protocol-abstract)). The message must include:
- Unique "public" key (`EK`) that Bob must use to encrypt messages.
- Unique "public" key (`EK`) that Bob must use for E2E key agreement.
- SMP server hostname and information to open secure encrypted transport connection (see [Appendix A](#appendix-a)).
@@ -170,9 +188,11 @@ To create and start using a simplex queue Alice and Bob follow these steps:
4. Alice receives Bob's message from the server using recipient queue ID `RID` (possibly, via the same transport connection she already has opened - see `message` in [Deliver queue message](#deliver-queue-message)):
1. She decrypts received message with "private" key `EK`.
1. She decrypts received message body using the secret `SS`.
2. Even though anybody could have sent the message to the queue with ID `SID` before it is secured (e.g. if communication is compromised), Alice would ignore all messages until the decryption succeeds (i.e. the result contains the expected message format). Optionally, in the client application, she also may identify Bob using the information provided, but it is out of scope of SMP protocol.
2. She decrypts received message with [key agreed with sender using] "private" key `EK`.
3. Even though anybody could have sent the message to the queue with ID `SID` before it is secured (e.g. if communication is compromised), Alice would ignore all messages until the decryption succeeds (i.e. the result contains the expected message format). Optionally, in the client application, she also may identify Bob using the information provided, but it is out of scope of SMP protocol.
5. Alice secures the queue `RID` with `"KEY"` command so only Bob can send messages to it (see [Secure queue command](#secure-queue-command)):
@@ -198,7 +218,7 @@ Bob now can securely send messages to Alice:
2. He signs `"SEND"` command to the server queue `SID` using the "private" key `SK` (that only he knows, used only for this queue).
3. He sends the command to the server (see `send` in [Send message](#send-message)), that the server will authenticate using the "public" key `SK` (that Alice earlier provided to the server).
3. He sends the command to the server (see `send` in [Send message](#send-message)), that the server will authenticate using the "public" key `SK` (that Alice earlier received from Bob and provided to the server via `"KEY"` command).
2. Alice receives the message(s):
@@ -266,7 +286,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 +294,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,48 +346,89 @@ 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.
## SMP commands
## Message delivery notifications
Commands syntax below is provided using [ABNF][8] with [case-sensitive strings extension][8a].
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.
Each transmission between the client and the server must have this format/syntax (after the decryption):
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.
```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
corrId = 1*(%x21-7F) ; any characters other than control/whitespace
queueId = encoded ; empty queue ID is used with "create" command
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 Transmission structure
Each transport block (SMP transmission) has a fixed size of 16384 bytes for traffic uniformity.
Some parts of SMP transmission are padded to a fixed size; this padding is uniformly added as a word16 encoded in network byte order - see `paddedString` syntax.
In places where some part of the transmission should be padded, the syntax for `paddedNotation` is used:
```
paddedString = originalLength string pad
originalLength = 2*2 OCTET
pad = N*N"#" ; where N = paddedLength - originalLength - 2
paddedNotation = <padded(string, paddedLength)>
; string - un-padded string
; paddedLength - required length after padding, including 2 bytes for originalLength
```
Each transmission between the client and the server must have this format/syntax:
```
paddedTransmission = <padded(transmission), 16384>
transmission = [signature] SP signed
signed = sessionIdentifier SP [corrId] SP [queueId] SP smpCommand
; corrId is required in client commands and server responses,
; it is empty in server notifications.
corrId = 1*32(%x21-7F) ; any characters other than control/whitespace
queueId = encoded ; max 32 bytes when decoded (24 bytes is used),
; empty queue ID is used with "create" command and in some server responses
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 = <base64 encoded binary>
```
`base64` encoding should be used with padding, as defined in section 4 of [RFC 4648][9]
## SMP commands
Commands syntax below is provided using [ABNF][8] with [case-sensitive strings extension][8a].
```abnf
smpCommand = ping / recipientCmd / send / subscribeNotifications / serverMsg
recipientCmd = create / subscribe / secure / enableNotifications /
acknowledge / suspend / delete
serverMsg = queueIds / message / notifierId / messageNotification /
unsubscribed / ok / error
```
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 +438,36 @@ 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
x509encoded = <base64 X509 key encoding>
create = %s"NEW " recipientSignaturePublicKey recipientDhPublicKey
recipientSignaturePublicKey = length x509encoded
; the recipient's Ed25519 or Ed448 public key to verify commands for this queue
recipientDhPublicKey = length x509encoded
; the recipient's Curve25519 key for DH exchange to derive the secret
; that the server will use to encrypt delivered message bodies
; using [NaCl crypto_box][16] encryption scheme (curve25519xsalsa20poly1305).
x509encoded = <binary X509 key encoding>
length = 1*1 OCTET
```
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
recipientId = encoded
senderId = encoded
queueIds = %s"IDS " recipientId senderId srvDhPublicKey
serverDhPublicKey = length x509encoded
; the server's Curve25519 key for DH exchange to derive the secret
; that the server will use to encrypt delivered message bodies to the recipient
recipientId = length *OCTET ; 16-24 bytes
senderId = length *OCTET ; 16-24 bytes
```
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 with empty queue ID (the third part of the transmission).
#### Subscribe to queue
@@ -401,19 +481,43 @@ 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 " senderSignaturePublicKey
senderSignaturePublicKey = length x509encoded
; the sender's Ed25519 or Ed448 key to verify SEND commands for this queue
```
`senderKey` is received from the sender as part of the first message - see [Send Message Command](#send-message-command).
`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 " notifierKey
notifierKey = length x509encoded
; the notifier's Ed25519 or Ed448 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 " notifierId
notifierId = length *OCTET ; 16-24 bytes
```
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:
@@ -461,10 +565,17 @@ Currently SMP defines only one command that can be used by senders - `send` mess
This command is sent to the server by the sender both to confirm the queue after the sender received out-of-band message from the recipient and to send messages after the queue is secured:
```abnf
send = %s"SEND" SP size SP msgBody SP
; the last SP is in addition to SP in the transmission
size = 1*DIGIT ; size in bytes
msgBody = *OCTET ; any binary content of specified size
send = %s"SEND " smpEncMessage
smpEncMessage = smpPubHeader sentMsgBody ; message up to 16088 bytes
smpPubHeader = smpClientVersion ("1" senderPublicDhKey / "0")
smpClientVersion = word16
senderPublicDhKey = length x509encoded
; sender's Curve25519 public key to agree DH secret for E2E encryption in this queue
; it is only sent in confirmation message
x509encoded = <binary X509 key encoding>
sentMsgBody = 16032*16032 OCTET
; E2E-encrypted smpClientMessage padded to 16032 bytes before encryption
word16 = 2*2 OCTET
```
The first message is sent to confirm the queue - it should contain sender's server key (see decrypted message syntax below) - this first message must be sent without signature.
@@ -482,15 +593,107 @@ Until the queue is secured, the server should accept any number of unsigned mess
The body should be encrypted with the recipient's "public" key (`EK`); once decrypted it must have this format:
```abnf
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
clientBody = *OCTET
sentMsgBody = <encrypted padded(smpClientMessage, 16032)>
smpClientMessage = smpPrivHeader clientMsgBody
smpPrivHeader = emptyHeader / smpConfirmationHeader
emptyHeader = " "
smpConfirmationHeader = %s"K" senderKey
senderKey = length x509encoded
; the sender's Ed25519 or Ed448 public key to sign SEND commands for this queue
clientMsgBody = *OCTET ; up to 16016 in case of emptyHeader
```
`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.
SMP transmission structure for sent messages:
```
------- transmission (= 16384 bytes)
2 | originalLength
276- | signature sessionId corrId queueId %s"SEND" SP (1+114 + 1+32? + 1+32 + 1+24 + 4+1 = 210)
....... smpEncMessage (= 16088 bytes = 16384 - 296 bytes)
8- | smpPubHeader (for messages it is only version and '0' to mean "no DH key" = 3 bytes)
24 | nonce for smpClientMessage
16 | auth tag for smpClientMessage
------- smpClientMessage (E2E encrypted, = 16032 bytes = 16088 - 48)
2 | originalLength
12- | smpPrivHeader
.......
| clientMsgBody (<= 16016 bytes = 16032 - 14)
.......
0+ | smpClientMessage pad
------- smpClientMessage end
|
....... smpEncMessage end
18+ | transmission pad
------- transmission end
```
SMP transmission structure for received messages:
```
------- transmission (= 16384 bytes)
2 | originalLength
276- | signature sessionId corrId queueId %s"MSG" SP msgId timestamp (1+114 + 1+32? + 1+32 + 1+24 + 3+1 + 24+1 + 8 = 243)
16 | auth tag (msgId is used as nonce)
------- serverEncryptedMsg (= 16090 bytes = 16384 - 294 bytes)
2 | originalLength
....... smpEncMessage (= 16088 bytes = 16090 - 2 bytes)
16- | smpPubHeader (empty header for the message)
24 | nonce for smpClientMessage
16 | auth tag for smpClientMessage
------- smpClientMessage (E2E encrypted, = 16032 bytes = 16088 - 56 bytes)
2 | originalLength
16- | smpPrivHeader (empty header for the message)
....... clientMsgBody (<= 16016 bytes = 16032 - 16)
-- TODO move internal structure (below) to agent protocol
20- | agentPublicHeader (the size is for user messages post handshake, without E2E X3DH keys - it is version and 'M' for the messages - 3 bytes in total)
....... E2E double-ratchet encrypted (<= 15996 bytes = 16016 - 20)
1 | encoded double ratchet header length (it is 123 now)
123 | encoded double ratchet header, including:
2 | version
16 | double-ratchet header iv
16 | double-ratchet header auth tag
1+88 | double-ratchet header (actual size is 69 bytes, the rest is reserved)
16 | message auth tag (IV generated from chain ratchet)
------- encrypted agent message (= 15856 bytes = 15996 - 140)
2 | originalLength
64- | agentHeader (the actual size is 41 = 8 + 1+32)
2 | %s"MM"
.......
| application message (<= 15788 bytes = 15856 - 68)
.......
0+ | encrypted agent message pad
------- encrypted agent message end
|
....... E2E double-ratchet encrypted end
|
-- TODO move internal structure (above) to agent protocol
....... clientMsgBody end
0+ | smpClientMessage pad
------- smpClientMessage end
|
....... smpEncMessage end
0+ | serverEncryptedMsg pad
------- serverEncryptedMsg end
0+ | transmission pad
------- transmission end
```
### 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,16 +707,38 @@ 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
msgId = encoded
timestamp = <date-time defined in RFC3339>
message = %s"MSG " msgId SP timestamp SP encryptedMsgBody
encryptedMsgBody = <encrypt paddedSentMsgBody> ; server-encrypted padded sent msgBody
paddedSentMsgBody = <padded(sentMsgBody, maxMessageLength + 2)> ; maxMessageLength = 16088
msgId = length 24*24OCTET
timestamp = 8*8OCTET
```
`msgId` - unique message ID generated by the server based on cryptographically strong random bytes. It should be used by the clients to detect messages that were delivered more than once (in case the transport connection was interrupted and the server did not receive the message delivery acknowledgement).
`msgId` - unique message ID generated by the server based on cryptographically strong random bytes. It should be used by the clients to detect messages that were delivered more than once (in case the transport connection was interrupted and the server did not receive the message delivery acknowledgement). Message ID is used as a nonce for server/recipient encryption of message bodies.
`timestamp` - the UTC time when the server received the message from the sender, must be in date-time format defined by [RFC 3339][10]
`timestamp` - system time when the server received the message from the sender as **a number of seconds** since Unix epoch (1970-01-01) encoded as 64-bit integer in network byte order. If a client system/language does not support 64-bit integers, until 2106 it is safe to simply skip the first 4 zero bytes and decode 32-bit unsigned integer (or as signed integer until 2038).
`binaryMsg` - see syntax in [Send message](#send-message)
`paddedSentMsgBody` - 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
@@ -528,23 +753,24 @@ No further messages should be delivered to unsubscribed transport connection.
#### Error responses
- incorrect block format, encoding or signature size (`BLOCK`).
- missing or different session ID - tls-unique binding of TLS transport (`SESSION`)
- command errors (`CMD`):
- error parsing command (`SYNTAX`)
- prohibited command (`PROHIBITED`) - any server response sent from client or `ACK` sent without active subscription or without message delivery.
- incorrect RSA key size in `NEW` or `KEY` commands - only 1024, 2048 and 4096-bit keys are allowed (`KEY_SIZE`).
- transmission has no required signature or queue ID (`NO_AUTH`)
- transmission has unexpected credentials (`HAS_AUTH`)
- transmission has no required queue ID (`NO_QUEUE`)
- authentication error (`AUTH`) - incorrect signature, unknown (or suspended) queue, sender's ID is used in place of recipient's and vice versa, and some other cases (see [Send message command](#send-message-command)).
- incorrect message body size (`SIZE`).
- authentication error (`AUTH`) - incorrect signature, unknown (or suspended) queue, sender's ID is used in place of recipient's and vice versa, and some other cases (see [Send message](#send-message) command).
- message queue quota exceeded error (`QUOTA`) - too many messages were sent to the message queue. Further messages can only be sent after the recipient retrieves the messages.
- sent message is too large (> 16088) to be delivered (`LARGE_MSG`).
- internal server error (`INTERNAL`).
The syntax for error responses:
```abnf
error = %s"ERR" SP errorType
errorType = %s"BLOCK" / %s"CMD" SP cmdError / %s"AUTH" / %s"SIZE" /%s"INTERNAL"
cmdError = %s"SYNTAX" / %s"PROHIBITED" / %s"KEY_SIZE" / %s"NO_AUTH" / %s"HAS_AUTH" / %s"NO_QUEUE"
error = %s"ERR " errorType
errorType = %s"BLOCK" / %s"SESSION" / %s"CMD " cmdError / %s"AUTH" / %s"LARGE_MSG" /%s"INTERNAL"
cmdError = %s"SYNTAX" / %s"PROHIBITED" / %s"NO_AUTH" / %s"HAS_AUTH" / %s"NO_QUEUE"
```
Server implementations must aim to respond within the same time for each command in all cases when `"ERR AUTH"` response is required to prevent timing attacks (e.g., the server should perform signature verification even when the queue does not exist on the server or the signature of different size is sent, using any RSA key with the same size as the signature size).
@@ -565,52 +791,52 @@ 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_CHACHA20_POLY1305_SHA256 cipher suite (for better performance on mobile devices),
- ed25519 and ed448 EdDSA algorithms for signatures,
- x25519 and x448 ECDHE groups for key exchange.
- servers must send the chain of exactly 2 self-signed certificates in the handshake, with the first (offline) certificate one signing the second (online) certificate. Offline certificate fingerprint is used as a server identity - it is a part of SMP server address.
- The clients must abort the connection in case a different number of certificates 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 fingerprint of the online server certificate 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 server should be `serverHello` and the client should respond with `clientHello` - these blocks are used to agree SMP protocol version:
```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)
serverHello = minSmpVersion maxSmpVersion sessionIdentifier pad
minSmpVersion = smpVersion
maxSmpVersion = smpVersion
sessionIdentifier = length *OCTET
; 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)
clientHello = smpVersion pad
; chosen SMP protocol version - it must be the maximum supported version
; within the range offered by the server
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
smpVersion = 2*2OCTET ; Word16 version number
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
pad = 1*OCTET
pad = *OCTET
```
For TLS 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 (possibly, with another channel binding).
[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
@@ -623,3 +849,7 @@ 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
[16]: https://nacl.cr.yp.to/box.html
+18
View File
@@ -0,0 +1,18 @@
Scheme name: simplex
Status: Provisional
Applications/protocols that use this scheme name:
This scheme is used for connection requests in SimpleX Agent Protocol,
a middle layer protocol for managing bi-directional communication via
redundant unidirectional SimpleX Messaging Protocol queues.
Contact: Evgeny Poberezkin <ep@simplex.chat>
Change controller: Evgeny Poberezkin <ep@simplex.chat>
References:
The syntax for connection requests in the latest version of SimpleX Agent Protocol:
https://github.com/simplex-chat/simplexmq/blob/v5/protocol/agent-protocol.md#connection-request
SimpleX Messaging Protocol:
https://github.com/simplex-chat/simplexmq/blob/v5/protocol/simplex-messaging.md
+16
View File
@@ -0,0 +1,16 @@
Scheme name: smp
Status: Provisional
Applications/protocols that use this scheme name:
This scheme is used for URIs of message queues in SimpleX Messaging Protocol,
a client-server protocol for asynchronous distributed unidirectional
message transmission via persistent message queues.
Contact: Evgeny Poberezkin <ep@simplex.chat>
Change controller: Evgeny Poberezkin <ep@simplex.chat>
References:
The syntax for message queue URIs in the latest version of SimpleX Messaging Protocol:
https://github.com/simplex-chat/simplexmq/blob/v5/protocol/simplex-messaging.md#smp-queue-uri
+8 -8
View File
@@ -26,20 +26,20 @@ Such connections that can be unidirectional or duplex can be managed by agents v
The protocol to create and manage such stream could be the following:
1. The sender client notifies the agent about the stream it has to create for a specific duplex connection (the connection must be duplex for it to be possible): `<conn_alias> SNEW <size>` (where `conn_alias` is the existing duplex connection, `size` is the size for the data to be transferred; 0 for unbounded streams), the response is `<conn_alias> STREAM <stream_id> <size>`.
1. The sender client notifies the agent about the stream it has to create for a specific duplex connection (the connection must be duplex for it to be possible): `<conn_id> SNEW <size>` (where `conn_id` is the existing duplex connection, `size` is the size for the data to be transferred; 0 for unbounded streams), the response is `<conn_id> STREAM <stream_id> <size>`.
2. The sender agent sends to the recipient agent the "envelope" `STREAM <stream_id> <size>`
3. The receiving agent would notify the client about availability of the stream by sending `<conn_alias> STREAM <stream_id> <size>` notification.
4. Receiving client would send `<conn_alias> SJOIN <stream_id>` command to the receiving agent.
3. The receiving agent would notify the client about availability of the stream by sending `<conn_id> STREAM <stream_id> <size>` notification.
4. Receiving client would send `<conn_id> SJOIN <stream_id>` command to the receiving agent.
5. Receiving agent provisions the SMP queue on the random SMP server (we planned to remove SMP server from NEW command anyway). Possibly, there will be a limited list of servers that support streams (that is un-throttled number of messages), and stream support can be communicated via welcome header and change with AUTH SMP command.
6. Receiving agent sends the envelop to sending agent `SJOIN <stream_id> <invitation>` (where invitation has the same format as in out-of-band message, but it has symmetric AES key and IV instead of asymmetric RSA key - the encryption scheme should codified in the invitation to make it generic).
7. Sending agent and receiving agent confirm and secure SMP queue as usual. This is still not solving the problem that the sending agent should sign each command. We might extend SMP protocol to create a new queue type that allows authenticating the sender only once per TCP session so that messages can be sent without signing - the server can see that these queues are special anyway, as there will be much faster traffic there.
8. Sending agent, once the queue is confirmed notifies the sending client that the stream is ready to accept data by sending notification `<conn_alias> SREADY <stream_id> <max_chunk_size>`.
9. Sending agent client can now send packets into the stream up to total `<size>` limit if it was specified by using command `<conn_alias> SPUT <stream_id> <chunk_no> <chunk_size> <binary> ` (the chunk size should not be bigger than `max_chunk_size`, the agent would pad it to this size before sending to the receiving agent)
8. Sending agent, once the queue is confirmed notifies the sending client that the stream is ready to accept data by sending notification `<conn_id> SREADY <stream_id> <max_chunk_size>`.
9. Sending agent client can now send packets into the stream up to total `<size>` limit if it was specified by using command `<conn_id> SPUT <stream_id> <chunk_no> <chunk_size> <binary> ` (the chunk size should not be bigger than `max_chunk_size`, the agent would pad it to this size before sending to the receiving agent)
10. Sending agent would send the chunk to the receiving agent in `CHUNK <chunk_no> <ts> <chunk_size> <binary>` envelope, where the first 4 bytes in decrypted binary is chunk size.
11. Receiving agent would send the chunk to the client as `<conn_alias> SDATA <stream_id> <chunk_no> <ts> <status> <chunk_size> <binary>` (possibly, with three IDs and timestamps as with messages).
12. Both the receiving and sending clients can terminate the stream with `<conn_alias> SCLOSE <stream_id>` command. The agent would send `SCLOSED <stream_id>` envelope and the opposite client would receive `<conn_alias> SCLOSED <stream_id>` notification - no more data will be accepted into this stream.
11. Receiving agent would send the chunk to the client as `<conn_id> SDATA <stream_id> <chunk_no> <ts> <status> <chunk_size> <binary>` (possibly, with three IDs and timestamps as with messages).
12. Both the receiving and sending clients can terminate the stream with `<conn_id> SCLOSE <stream_id>` command. The agent would send `SCLOSED <stream_id>` envelope and the opposite client would receive `<conn_id> SCLOSED <stream_id>` notification - no more data will be accepted into this stream.
13. If the stream was bounded with some `size` then both the sending and receiving client would receive `SCLOSED` notification after the last chunk of correct size was sent (and the incorrect chunk would be rejected).
14. If the stream was interrupted then the recipient client can request resuming this stream using `<conn_alias> SRESUME <stream_id> <from_chunk_no>`, the recipient agent would send "envelope" `SRESUME <stream_id> <from_chunk_no> <invitation>`.
14. If the stream was interrupted then the recipient client can request resuming this stream using `<conn_id> SRESUME <stream_id> <from_chunk_no>`, the recipient agent would send "envelope" `SRESUME <stream_id> <from_chunk_no> <invitation>`.
To be clarified:
1. Possibly the internal stream ID used by clients should be different from stream ID communicated between agents, and in any case it should be connection scoped (same as with messages).
-25
View File
@@ -1,25 +0,0 @@
# SMP agent groups
## Problems
- device/user profile synchronisation
- chat group communication
Both problems would require message broadcast between a group of SMP agents.
## Solution: basic symmetric groups via SMP agent protocol
Additional commands and message envelopes to SMP agent protocol to provide an abstraction layer for device synchronisation and chat groups.
The groups are fully symmetric, all agent who are members of the group have equal rights and can join and leave group at any time.
All the information about the groups is stored only in agents, the commands are used to synchronise the group state between the agents.
```abnf
group_command = create_group / add_to_group / remove_from_group / leave_group
group_response = group_created / added_to_group / removed_from_group
group_notification = added_to_group_by / removed_from_group_by / left_group
create_group = %s"GNEW " group_name ; cAlias must be empty
add_to_group = %s"GADD " group_name ; cAlias is the connection to add to the group
added_to_group = %s"GADDED " name ; cAlias is the connection added to the group
```
+52
View File
@@ -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_id? 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>::<server1>/<queue_id1>,<server2>/<queue_id2>::<key1>`
`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).
@@ -31,7 +31,7 @@ Agent disconnected from host:port
### Received command from the client
```
n --> A : corrId connAlias parsed_command // raw_command
n --> A : corrId connId parsed_command // raw_command
```
`raw_command` is added only in case of parsing failure.
@@ -53,13 +53,13 @@ In case the response is a message or notification, corrId should be replaced wit
### Interpreted ("unwrapped") SMP message as agent message
```
Agent msg : connAlias parsed_message // raw_message
Agent msg : connId parsed_message // raw_message
```
### Sent response / message to the client
```
n <-- A : corrId connAlias parsed_command // raw_command
n <-- A : corrId connId parsed_command // raw_command
```
### Database changes
@@ -145,9 +145,9 @@ For each message, the agent performs the following actions to encrypt it:
3. Encrypt the message using this AES256 key with [AES-GCM-SIV](https://hackage.haskell.org/package/cryptonite-0.28/docs/Crypto-Cipher-AESGCMSIV.html) (AEAD scheme) using the function `encrypt` with a random nonce.
4. Encrypt the AES256 key with public encryption key for the queue using RSA-OAEP [encrypt](https://hackage.haskell.org/package/cryptonite-0.28/docs/Crypto-PubKey-RSA-OAEP.html#v:encrypt) function parameterized with [SHA256](https://hackage.haskell.org/package/cryptonite-0.28/docs/Crypto-Hash-Algorithms.html#t:SHA256) algorithm.
5. The message to send is concatenation of:
1. encrypted AES256 key,
2. AuthTag from encryption in step 3,
3. encrypted message (in this order)
1. encrypted AES256 key,
2. AuthTag from encryption in step 3,
3. encrypted message (in this order).
As there is no additional data that is sent e2e in clear text, we potentially need some simpler algorithm, possibly the one implemented in the tutorial, that implements authenticated encryption without additional (unencrypted) data.
@@ -155,7 +155,7 @@ Or we could use this algorithm to allow some data that is sent e2e in clear text
### Message decryption
Agent decrypt the message following the same steps in the opposite order
Agent decrypts the message following the same steps in the opposite order:
1. Split encrypted message to AES256 key, AuthTag and encrypted message - tag and key have fixed size (?).
2. Decrypt AES256 key using the decryption key (the private key that recipient has).
+146
View File
@@ -0,0 +1,146 @@
# Overview of SMP agent protocol commands
## Connections
### Commands and messages
A initiates connection, B accepts
- command `C:idB? NEW` - create connection
- message `C:idB INV cInv`
- command `C:idA? JOIN cInv replyMode` - join connection (response `OK`, followed by `CON`)
- *message* `C:idB REQ prv:invId infoB` - request from B joining sent to A (not implemented)
- *command* `C:idB ACPT prv:invId` - A confirms B joining (not implemented)
- message `C:id CON` - connection is established
- command `C:id SUB` - subscribe to connection
- message `C:id END` - unsubscribed from connection
- command `C:idB SEND msg` - send message
- message `C:idA SENT msgId` - confirmation that the message is sent
- message `C:id MSG msgId msgMeta msgIntegrity msgBody` - received message
- *command* `C:idB ACK msgId` - acknowledge message reception (not implemented)
- *message* `C:idA RCVD msgId msgIntegrity` - confirmation of message reception and integrity (not implemented)
- command `C:id OFF` - suspend connection
- command `C:id DEL` - delete connection
- message `C:id? OK` - command confirmation
- message `C:id? ERR e` - error
### Envelopes
- `MSG `
- `HELLO verificationKey ackMode`
- `REPLY replyInv`
## Broadcasts
### Commands & messages
- command `B:id? NEW` - create broadcast (response is `B:id OK`)
- command `B:id SEND msg` - broadcast message (response is multiple `C:id SENT msgId` or ERR, separately for each connection, followed by `B:id SENT msgId` once sent to all)
- message `B:id SENT msgId` - notification that the message is sent and its internal ID, same as SENT
- command `B:id ADD cId` - add existing connection to a broadcast (response is `B:id OK` or `ERR`, e.g. if bId is used)
- command `B:id REM cId` - remove connection from the broadcast (response is `REMD`)
- message `B:id REMD cId` - connection removed from the broadcast
- message `B:id EMPTY` - all connections were removed from the broadcast
- command `B:id DEL` - delete broadcast (response is `B:id OK`)
- command `B:id LS` - list connections in broadcast, response is `B:id MEM space_separated_connections`
- message `B:id MEM space_separated_connections`
## Open/public connection
### Commands
- command `O:id? NEW` - create open connection
- message `O:id INV oInv` - open invitation
- command `C:id? JOIN oInv replyMode` - join connection (response `OK`, followed by `CON`)
- message `O:id REQ open:invId infoB` - confirmation from B joining sent to A
- command `C:idC? ACPT open:invId` - note, that it creates new connection, keeping OPEN connection
- command `O:id SUB` - subscribe to open connection
- message `O:id END` - unsubscribed from open connection
- command `O:id OFF` - suspend open connection
- command `O:id DEL` - delete open connection
- message `O:id? OK` - command confirmation
- message `O:id? ERR e` - error
## Introductions
### Commands
- command `C:idAB INTRO C:idAM infoM` - introduce connection cIdB to connection cIdM (response is `OK`)
- message `C:idBA REQ C:invId infoM` - notification to confirm introduction
- command `C:idBM? ACPT C:invId` - accept offer to be introduced (response is `cIdBM OK`, followed by `ICON`)
- message `C:idBM CON` - confirmation that connection is established to both introduced parties
- message `C:idAB CON C:idAM` - confirmation that connection is established to the introducer
### Envelopes
- `INTRO C:extIntroIdM infoM` - new introduction offered by introducer
- `INV C:extIntroIdB prv:invBM infoB` - invitation to join connection from B to M sent via A (can be pub:)
- `REQ C:extIntroIdB prv:invBM infoB` - new introduction forwarded by introducer
- `CON C:extIntroIdM` - confirmation that the connection is established sent by both introduced parties to the introducer
## Groups
## Agent commands and messages syntax
- command `G:gId? NEW` - create group (response is `G:gId OK`)
- command `C:cId INTRO G:gId gInfo` - add existing connection to a group
- message `C:cId REQ g:invID gInfo` - invitation to join the group
- command `G:gId? ACPT g:invId` - accept invitation (response is `G gId OK`)
- message `G:gId CON C:cId` - 2 connections created with some group member (both for group and direct messages)
- message `G:gId MEM [C:cId]` - connection created with all group members for a given member or current client
- command `G:gId SEND msg` - send message to group
- message `G:gId SENT msgId` - notification that the message is sent and its internal ID, same as SENT
- message `G:gId MSG C:cId msgId msgdata` - received group message from cId, msgdata is the same set of parameters as in `MSG`
- command `G:gId ACK msgId` - acknowledge message reception by the client
- message `G:gId RCVD t:cId msgId status` - message delivery notification
- command `G:gId LEAVE` - leave the group
- message `G:gId LEFT [C:cId]` - connection cId left the group
- command `G:gId REM C:cId` - remove group member (response is `gId OK`, followed by `GREMD` notification)
- message `G:gId REMD C:cId [C:cId]` - member removed
- message `G:gId OUT C:cId` - you are removed (see question below - should it be just a sequence of GLEFT?)
- message `G:gId EMPTY` - all members left the group and it is now empty
- command `G:gId DEL` - delete the group (response is `gId OK`)
- message `G:gId DELD [C:cId]` - group deleted
## Agent message envelopes syntax
- `GROUP C:mid G:inv gInfo` - invitation to join the group
- `MEM C:mid` - confirmation that member connected to all members
- `LEFT` - notification that member left the group
- `OUT` - you are removed from the group
- `REM C:mid` - remove member mid from the group
- `REMD C:mid` - confirmation that member is removed
- `DEL` - group is deleted
- `DELD` - confirmation that group is deleted
## Commands and objects
| Dir | Command / message | (C)onnection | (O)pen connection | (B)roadcast | (G)roup |
|:---------:|:--------------------:|:------------:|:-----------------:|:-----------:|:-------:|
| command | `t:id? NEW` | ✓ | ✓ | ✓ | ✓ |
| command | `C:id INTRO t:id info` | ✓ | - | - | ✓ |
| message | `t:id INV inv` | ✓ | ✓ | - | - |
| command | `C:id? JOIN inv replyMode info` | ✓ | - | - | - |
| message | `t:id REQ invId info` | ✓ | ✓ | - | ✓ |
| command | `t:id? ACPT invId` | ✓ | - | - | ✓ |
| message | `t:id CON [C:id]` | ✓ | - | - | ✓ |
| message | `t:id MEM [C:id]` | - | - | - | ✓ |
| command | `t:id SUB` | ✓ | ✓ | - | ✓ |
| message | `t:id END` | ✓ | ✓ | - | ✓ |
| command | `t:id OFF` | ✓ | ✓ | - | - |
| command | `t:id DEL` | ✓ | ✓ | ✓ | ✓ |
| message | `t:id DELD [C:Id]` | ✓ | ✓ | ✓ | ✓ |
| command | `t:id SEND msg` | ✓ | - | ✓ | ✓ |
| message | `t:id SENT [t':id] msgId` | ✓ | - | ✓ | ✓ |
| message | `t:id MSG [C:id] msgId msgdata` | ✓ | - | - | ✓ |
| command | `t:id ACK msgId` | ✓ | - | - | ✓ |
| message | `t:id RCVD [t':id] msgId status` | ✓ | - | - | ✓ |
| command | `t:id ADD C:id` | - | - | - | ✓ |
| command | `t:id REM C:id` | - | - | ✓ | ✓ |
| command | `t:id REMD C:id` | - | - | ✓ | ✓ |
| message | `t:id EMPTY` | - | - | ✓ | ✓ |
| message | `G:id OUT C:id` | - | - | - | ✓ |
| command | `t:id LS` | - | - | ✓ | ✓ |
| message | `t:id MS cIds` | - | - | ✓ | ✓ |
| command | `G:id LEAVE` | - | - | - | ✓ |
| message | `G:id LEFT [C:id]` | - | - | - | ✓ |
+50
View File
@@ -0,0 +1,50 @@
# SMP agent broadcast
## Problem
Support agent message broadcast to multiple connections.
It is done in ad-hoc way as part of the previous [groups proposal](./2021-05-23-groups2.md) - this proposal defines broadcast as a separate agent primitive to simplify group management.
It can also be used for other purposes when the same message needs to be sent to multiple recipients without creating groups.
## Solution
A minimal protocol of additional client commands to create, manage and use broadcasts.
From the point of view of the recipient this will look like a normal message, as if the sending agent executed multiple send commands (in fact, broadcast can be implemented by agent sending itself multiple SEND commands)
### Commands and messages
- command `B:bId? NEW` - create broadcast (response is `B:bId OK`, or `ERR` if broadcast already exists)
- command `B:bId ADD C:cId` - add existing connection to a broadcast (response is `B:bId OK` or `ERR`, e.g. if connection already added or does not exist)
- command `B:bId SEND msg` - broadcast message (response is multiple `B:cId SENT [C:bId] msgId` or ERR, separately for each connection and then for the broadcast)
- message `B:bId SENT [C:bId] msgId` - notification that the message is sent to a specific or all recipients
- command `B:bId REM C:cId` - remove connection from broadcast (response is `B:bId OK` or `ERR`)
- message `B:bId EMPTY` - all connections were removed from the broadcast
- command `B:bId DEL` - delete broadcast (response is `B:bId OK` and when the last connection is removed an additional `B:bId EMPTY` is sent)
- command `B:bId LS` - list connections in broadcast, response is `B:Id MS space_separated_connections`
- message `B:bId MS space_separated_connections`
## Questions
1. Should broadcast IDs use the same namespace as connection IDs (and as group IDs)? Having the same namespace for all abstractions that the agent can operate on can be helpful, as it can also allow implementing some queries to determine which type a given ID has, but it also increases implementation complexity.
2. Given that this abstraction would be used as internal abstraction for groups (same as connections internal to the group), it might be better to implement "agent users", each with its own connection namespace. In this case agent would use itself as one of the users.
3. There is a similarity of commands for connections, groups and broadcasts, they only differ on the single-letter prefix. We could do one of the following:
- use the same command for different object types. This feels incorrect and error prone on its own.
- extend transmission structure with the field defining the object type (connection, group, broadcast, etc.).
In this case, the transmission would look like:
```
agentTransmission = [corrId] CRLF objectType:[objectID] CRLF agentCommand
objectType = C | B | G ; this is the additional field
```
This approach would allow reusing the existing command avoiding the unnecessary repetition.
In this case, the command type could be parameterized with the list of supported agent object types, so we can ensure on the type level that only allowed commands can be constructed.
EDIT: This approach is already implemented
File diff suppressed because one or more lines are too long
+41
View File
@@ -0,0 +1,41 @@
# SMP agent introduction
## Problem
Allow an agent client to connect two connections it has directly, with the agent acting as an out-of-band channel.
It can be used both separately as part of some client functionality, and as part of group protocol.
## Solution
A protocol with commands and message envelopes to exchange the information between parties to establish connection.
### Commands and messages
Below commands are for the scenario when A introduces B to M.
- command `C:idAB INTRO C:idAM infoM` - initiate introduction of the connection cIdB to connection cIdM (response is `C:idAB OK`)
- message `C:idBA REQ C:invId infoM` - notification to confirm introduction
- command `C:idBM? ACPT C:invId` - accept offer to be introduced (response is `C:idBM OK`, followed by `C:idBM CON`)
- message `C:idBM CON` - confirmation that connection is established to both introduced parties
- message `C:idAB CON C:idAM` - confirmation that connection is established to the introducer
### Agent envelopes
- `INTRO C:extIntroIdM infoM` - new introduction offered by introducer
- `INV C:extIntroIdB prv:invBM infoB` - invitation to join connection from B to M sent via A
- `REQ C:extIntroIdB prv:invBM infoB` - new introduction forwarded by the introducer
- `CON C:extIntroId` - confirmation that the connection is established sent by both introduced parties to the introducer
## Namespace
Given that the introduction objects are short lived, they should not reuse the same commands or share the same namespace as connections, broadcasts and groups, but they probably should share the namespace with group and connection invitations.
## Introduction protocol costs
5 messages + cost to establish a connection
The [sequence digram for introduction](https://mermaid.ink/img/eyJjb2RlIjoic2VxdWVuY2VEaWFncmFtXG4gIHBhcnRpY2lwYW50IEEgYXMgQWxpY2UgKEEpIC0gdGhlIGludHJvZHVjZXJcbiAgcGFydGljaXBhbnQgQUEgYXMgQWxpY2Unczxicj5hZ2VudCAoQUEpXG4gIHBhcnRpY2lwYW50IEIgYXMgQm9iIChCKSAtIGludHJvZHVjZWRcbiAgcGFydGljaXBhbnQgQkEgYXMgQm9iJ3M8YnI-YWdlbnQgKEJBKVxuICBwYXJ0aWNpcGFudCBNIGFzIE1hcmsgKE0pIC0gaW50cm9kdWNlZCB0b1xuICBwYXJ0aWNpcGFudCBNQSBhcyBNYXJrJ3M8YnI-YWdlbnQgKE1BKVxuXG4gIG5vdGUgb3ZlciBBLCBBQTogMS4gY3JlYXRlIGludHJvZHVjdGlvblxuICBBIC0-PiBBQTogQzppZEFCIElOVFJPIEM6aWRBTSBpbmZvTTxicj4oaWRBQiAtIGNvbm4gYWxpYXMgQSBoYXMgZm9yIEIsPGJyPmlkQU0gLSBmb3IgTSlcbiAgQUEgLT4-IEE6IEM6aWRBQiBPS1xuXG4gIG5vdGUgb3ZlciBBLCBCQTogMi4gc2VuZCBpbnRybyB0byBCb2JcblxuICBBQSAtPj4gQkE6IHZpYSBpZEFCOiBJTlRSTyBDOmV4dEludHJvSWRNIGluZm9NXG4gIEJBIC0-PiBCOiBDOmlkQkEgUkVRIEM6aW50SW50cm9JZE0gaW5mb01cbiAgQiAtPj4gQkE6IEM6aWRCTT8gQUNQVCBDOmludEludHJvSWRNXG4gIEJBIC0-PiBCOiBDOmlkQk0gT0tcblxuICBub3RlIG92ZXIgQkE6IDMuIGNyZWF0ZSBjb25uZWN0aW9uIGZvciAgTSBpZEJNXG5cbiAgQkEgLT4-IEFBOiB2aWEgaWRCQTogSU5WIEM6ZXh0SW50cm9JZE0gcHJ2OmludkJNIGluZm9CXG5cbiAgbm90ZSBvdmVyIEFBLCBNOiA0LiBzZW5kIGludHJvIHRvIE1hcmtcblxuICBBQSAtPj4gTUE6IHZpYSBpZEFNOiBSRVEgQzpleHRJbnRyb0lkQiBwcnY6aW52Qk0gaW5mb0JcblxuICBub3RlIG92ZXIgTUEsIEI6IDUuIE1hcmsgY29ubmVjdHMgdG8gQm9iXG5cbiAgTUEgLT4-IE06IEM6aWRNQSBSRVEgQzppbnRJbnRyb0lkQiBpbmZvQlxuICBNIC0-PiBNQTogQzppZE1CPyBBQ1BUIEM6aW50SW50cm9JZEJcbiAgTUEgLT4-IE06IEM6aWRNQiBPS1xuXG4gIE1BIC0-PiBCQTogIGVzdGFibGlzaCBjb25uZWN0aW9uIGlkQk0gLT4gaWRNQlxuXG4gIG5vdGUgb3ZlciBBLCBNQTogNi4gbm90aWZ5IGFsbCBjbGllbnRzXG5cbiAgTUEgLT4-IE06IEM6aWRNQiBDT05cbiAgTUEgLT4-IEFBOiB2aWEgaWRNQTogQ09OIEM6ZXh0SW50cm9JZEJcbiAgQkEgLT4-IEI6IEM6aWRCTSBDT05cbiAgQkEgLT4-IEFBOiB2aWEgaWRCQTogQ09OIEM6ZXh0SW50cm9JZEJcbiAgQUEgLT4-IEE6IEM6aWRBQiBDT04gQzppZEFNXG4iLCJtZXJtYWlkIjp7fSwidXBkYXRlRWRpdG9yIjpmYWxzZX0), the source is [here](./intro.mmd).
![sequence digram for introduction](https://mermaid.ink/svg/eyJjb2RlIjoic2VxdWVuY2VEaWFncmFtXG4gIHBhcnRpY2lwYW50IEEgYXMgQWxpY2UgKEEpIC0gdGhlIGludHJvZHVjZXJcbiAgcGFydGljaXBhbnQgQUEgYXMgQWxpY2Unczxicj5hZ2VudCAoQUEpXG4gIHBhcnRpY2lwYW50IEIgYXMgQm9iIChCKSAtIGludHJvZHVjZWRcbiAgcGFydGljaXBhbnQgQkEgYXMgQm9iJ3M8YnI-YWdlbnQgKEJBKVxuICBwYXJ0aWNpcGFudCBNIGFzIE1hcmsgKE0pIC0gaW50cm9kdWNlZCB0b1xuICBwYXJ0aWNpcGFudCBNQSBhcyBNYXJrJ3M8YnI-YWdlbnQgKE1BKVxuXG4gIG5vdGUgb3ZlciBBLCBBQTogMS4gY3JlYXRlIGludHJvZHVjdGlvblxuICBBIC0-PiBBQTogQzppZEFCIElOVFJPIEM6aWRBTSBpbmZvTTxicj4oaWRBQiAtIGNvbm4gYWxpYXMgQSBoYXMgZm9yIEIsPGJyPmlkQU0gLSBmb3IgTSlcbiAgQUEgLT4-IEE6IEM6aWRBQiBPS1xuXG4gIG5vdGUgb3ZlciBBLCBCQTogMi4gc2VuZCBpbnRybyB0byBCb2JcblxuICBBQSAtPj4gQkE6IHZpYSBpZEFCOiBJTlRSTyBDOmV4dEludHJvSWRNIGluZm9NXG4gIEJBIC0-PiBCOiBDOmlkQkEgUkVRIEM6aW50SW50cm9JZE0gaW5mb01cbiAgQiAtPj4gQkE6IEM6aWRCTT8gQUNQVCBDOmludEludHJvSWRNXG4gIEJBIC0-PiBCOiBDOmlkQk0gT0tcblxuICBub3RlIG92ZXIgQkE6IDMuIGNyZWF0ZSBjb25uZWN0aW9uIGZvciAgTSBpZEJNXG5cbiAgQkEgLT4-IEFBOiB2aWEgaWRCQTogSU5WIEM6ZXh0SW50cm9JZE0gcHJ2OmludkJNIGluZm9CXG5cbiAgbm90ZSBvdmVyIEFBLCBNOiA0LiBzZW5kIGludHJvIHRvIE1hcmtcblxuICBBQSAtPj4gTUE6IHZpYSBpZEFNOiBSRVEgQzpleHRJbnRyb0lkQiBwcnY6aW52Qk0gaW5mb0JcblxuICBub3RlIG92ZXIgTUEsIEI6IDUuIE1hcmsgY29ubmVjdHMgdG8gQm9iXG5cbiAgTUEgLT4-IE06IEM6aWRNQSBSRVEgQzppbnRJbnRyb0lkQiBpbmZvQlxuICBNIC0-PiBNQTogQzppZE1CPyBBQ1BUIEM6aW50SW50cm9JZEJcbiAgTUEgLT4-IE06IEM6aWRNQiBPS1xuXG4gIE1BIC0-PiBCQTogIGVzdGFibGlzaCBjb25uZWN0aW9uIGlkQk0gLT4gaWRNQlxuXG4gIG5vdGUgb3ZlciBBLCBNQTogNi4gbm90aWZ5IGFsbCBjbGllbnRzXG5cbiAgTUEgLT4-IE06IEM6aWRNQiBDT05cbiAgTUEgLT4-IEFBOiB2aWEgaWRNQTogQ09OIEM6ZXh0SW50cm9JZEJcbiAgQkEgLT4-IEI6IEM6aWRCTSBDT05cbiAgQkEgLT4-IEFBOiB2aWEgaWRCQTogQ09OIEM6ZXh0SW50cm9JZEJcbiAgQUEgLT4-IEE6IEM6aWRBQiBDT04gQzppZEFNXG4iLCJtZXJtYWlkIjp7fSwidXBkYXRlRWRpdG9yIjpmYWxzZX0)
+135
View File
@@ -0,0 +1,135 @@
sequenceDiagram
participant M as Existing<br>member (M)
participant MA as Existing<br>member<br>agent (MA)
participant A as Alice (A)
participant AA as Alice's<br>agent (AA)
participant BA as Bob's<br>agent (BA)
participant B as Bob (B)
note over A, AA: 1. create new group (no members)
A ->> AA: G:gidA? NEW<br>(gidA - group ID of this group for A,<br>can be generated by the agent)
note over AA: create "internal" broadcast associated with the group<br>(B: NEW)
AA ->> A: G:gidA OK
note over A, BA: 2. add Bob to group
A ->> AA: C:idAB INTRO G:gidA gInfo<br>(idAB - conn alias A has for B)
note over AA: generate new random ID for member B (midB, unique per group)<br>initiate "internal" connection gidAB for B in group<br>(internal means that it is not visible to the clients<br>and cannot be used with client commands)
AA ->> BA: via idAB: GROUP C:midB g:gInvAB gInfo
BA ->> B: C:idBA REQ G:invID gInfo<br>(invID - to refer to it in ACPT)
B ->> BA: G:gidB? ACPT G:invID<br>(RJCT G:invID could be added)
note over BA: create group and "internal" broadcast associated with the group<br>(B: NEW)
BA ->> B: G:gidB OK
BA ->> AA: establish internal connection gidBA (using g:gInvAB) for A in group
note over BA, AA: add connections gidBA and gidAB to broadcasts<br>(B: ADD)
AA ->> A: G:gidA CON C:idAB
BA ->> B: G:gidA CON C:idBA
note over M, B: For each existing member M:<br>create and accept internal introduction between connections, related to the group, via gidAB/BA/AM/MA, connections created are gidBM and gidMB<br>The fact that the introduction arrives via connection allocated for the group, allows agents identify it as a new group member, ID used in introductions is group-scoped member ID.
note over A, BA: once all members were sent to B
AA ->> BA: via gidAB: MEM C:midB
note over BA, B: once all members are connected
BA ->> B: G:gidB MEM
note over A, AA: once all members reported connection
AA ->> A: G:gidA MEM C:idAB
note over M, AA: for each member M
AA ->> MA: via gidAM: MEM C:midB
MA ->> M: G:gidM MEM C:idMB
note over M, B: 3. B sends message to the group
B ->> BA: G:gidB SEND msg
note over BA: send message via associated broadcast and respond to client with SENT notifications
BA ->> B: G:gidB SENT C:idBA intMsgID
BA ->> B: G:gidB SENT C:idBM intMsgID
note over BA, B: once sent to all
BA ->> B: G:gidB SENT intMsgID
AA ->> A: G:gidA MSG C:idAB intMsgID msgdata
A ->> AA: G:gidA ACK intMsgID
AA ->> BA: via gidAB: RCVD extMsgID hash sig
BA ->> B: G:gidA RCVD C:idBA intMsgID status<br>(status - message integrity check)
MA ->> M: G:gidM MSG C:idMB intMsgID msgdata
M ->> MA: G:gidM ACK intMsgID
MA ->> BA: via gidMB: RCVD extMsgID hash sig
BA ->> B: G:gidM RCVD C:idBM intMsgID status
note over BA, B: once received by all
BA ->> B: G:gidM RCVD intMsgID status
note over M, B: 4a. A leaves group
A ->> AA: G:gidA LEAVE
AA ->> A: G:gidA OK
AA ->> BA: via gidAB: LEFT
note over AA: remove gidAB, remove from broadcast
note over BA: remove gidBA, remove from broadcast
BA ->> B: G:gidB LEFT C:idBA
AA ->> MA: via gidAM: LEFT
note over AA: remove gidAM, remove from broadcast
note over MA: remove gidMA, remove from broadcast
MA ->> M: G:gidM LEFT C:idMA
AA ->> A: G:gidA LEFT
note over B, BA: if all members left
BA ->> B: G:gidB: EMPTY
note over M, B: 4b. A removes B from group
A ->> AA: G:gidA REM C:idAB
AA ->> A: G:gidA OK
AA ->> BA: via gidAB: OUT
note over BA: remove gidBA, all gidBM
BA ->> B: G:gidB OUT C:idBA
note over AA: remove gidAB
AA ->> A: G:gidA OK
note over M, B: below steps happen for each existing member M
AA ->> MA: via gidAM: REM C:midB
note over MA: remove gidMB
MA ->> AA: via gidMA: REMD C:midB
MA ->> M: G:gidM REMD C:idMB C:idMA<br>(B removed by A)
note over A, AA: once all members removed B
AA ->> A: G:gidA REMD C:idAB<br>(B removed by this agent)
note over M, B: 4c. A deletes group
A ->> AA: G:gidA DEL
AA ->> A: G:gidA OK
AA ->> BA: via gidAB: DEL
note over BA: remove all group connections and messages
BA ->> B: G:gidB DELD C:idBA<br>(group deleted by A)
BA ->> AA: via gidBA: DELD
AA ->> A: G:gidA DELD C:idAB
AA ->> MA: via gidAM: DEL
note over MA: remove all group connections and messages
MA ->> M: G:gidM DELD C:idMA<br>(group deleted by A)
MA ->> AA: via gidMA: DELD
AA ->> A: G:gidA DELD C:idAM
AA ->> A: G:gidA DELD<br>(group deleted by this agent - all confirmed)
+42
View File
@@ -0,0 +1,42 @@
sequenceDiagram
participant A as Alice (A) - the introducer
participant AA as Alice's<br>agent (AA)
participant B as Bob (B) - introduced
participant BA as Bob's<br>agent (BA)
participant M as Mark (M) - introduced to
participant MA as Mark's<br>agent (MA)
note over A, AA: 1. create introduction
A ->> AA: C:idAB INTRO C:idAM infoM<br>(idAB - conn alias A has for B,<br>idAM - for M)
AA ->> A: C:idAB OK
note over A, BA: 2. send intro to Bob
AA ->> BA: via idAB: INTRO C:extIntroIdM infoM
BA ->> B: C:idBA REQ C:intIntroIdM infoM
B ->> BA: C:idBM? ACPT C:intIntroIdM
BA ->> B: C:idBM OK
note over BA: 3. create connection for M idBM
BA ->> AA: via idBA: INV C:extIntroIdM invBM infoB
note over AA, M: 4. send intro to Mark
AA ->> MA: via idAM: REQ C:extIntroIdB invBM infoB
note over MA, B: 5. Mark connects to Bob
MA ->> M: C:idMA REQ C:intIntroIdB infoB
M ->> MA: C:idMB? ACPT C:intIntroIdB
MA ->> M: C:idMB OK
MA ->> BA: establish connection idBM -> idMB
note over A, MA: 6. notify all clients
MA ->> M: C:idMB CON
MA ->> AA: via idMA: CON C:extIntroIdB
BA ->> B: C:idBM CON
BA ->> AA: via idBA: CON C:extIntroIdB
AA ->> A: C:idAB CON C:idAM
@@ -0,0 +1,18 @@
# SMP server image for DigitalOcean
## How to build an image
1. [Create an API token](https://cloud.digitalocean.com/account/api/tokens) in vendor account in DigitalOcean.
2. Install [packer](https://www.packer.io/downloads) downloading binary or with brew (on Mac):
```shell
brew tap hashicorp/tap
brew install hashicorp/tap/packer
```
3. Run `packer build` in the `smp-server-digitalocean-droplet` repository:
```shell
cd ./scripts/smp-server-digitalocean-droplet
DIGITALOCEAN_TOKEN=$YOUR_TOKEN packer build -on-error=ask -color=false ./marketplace-image.json
```
@@ -0,0 +1,51 @@
#!/bin/bash
# Download latest release
bin_dir="/opt/simplex/bin"
binary="$bin_dir/smp-server"
mkdir -p $bin_dir
curl -L -o $binary https://github.com/simplex-chat/simplexmq/releases/latest/download/smp-server-ubuntu-20_04-x86-64
chmod +x $binary
# / Add to PATH
cat > /etc/profile.d/simplex.sh << EOF
#!/bin/bash
export PATH="$PATH:$bin_dir"
EOF
# Add to PATH /
# Source and test PATH
source /etc/profile.d/simplex.sh
smp-server --version
# Initialize server
ip_address=$(curl ifconfig.me)
smp-server init -l --ip $ip_address
# Server fingerprint
fingerprint=$(cat /etc/opt/simplex/fingerprint)
# Set up welcome script
echo "bash /opt/simplex/on_login.sh $fingerprint $ip_address" >> /root/.bashrc
# / Create systemd service for SMP server
cat > /etc/systemd/system/smp-server.service << EOF
[Unit]
Description=SMP server
[Service]
Type=simple
ExecStart=/bin/sh -c "exec $binary start >> /var/opt/simplex/smp-server.log 2>&1"
[Install]
WantedBy=multi-user.target
EOF
# Create systemd service for SMP server /
# Start systemd service for SMP server
chmod 644 /etc/systemd/system/smp-server.service
sudo systemctl enable smp-server
sudo systemctl start smp-server
@@ -0,0 +1,17 @@
#!/bin/bash
fingerprint=$1
server_address=$2
cat <<EOF
********************************************************************************
SMP server address: smp://$fingerprint@$server_address
Check SMP server status with: systemctl status smp-server
To keep this server secure, the UFW firewall is enabled.
All ports are BLOCKED except 22 (SSH), 443 (HTTPS), 5223 (SMP server).
********************************************************************************
To stop seeing this message delete line - bash /opt/simplex/on_login.sh - from /root/.bashrc
EOF
@@ -0,0 +1,12 @@
#!/bin/bash
set -eu
if [[ ! -f /opt/simplex/do_initialize_server ]]; then
touch /opt/simplex/do_initialize_server
elif [[ ! -f /etc/opt/simplex/smp-server.ini ]]; then
chmod +x /opt/simplex/initialize_server.sh
/opt/simplex/initialize_server.sh
else
echo "SMP server already initialized"
fi
@@ -0,0 +1,47 @@
{
"variables": {
"token": "{{env `DIGITALOCEAN_TOKEN`}}",
"image_name": "smp-server-snapshot",
"application_name": "SMP server"
},
"sensitive-variables": ["token"],
"builders": [
{
"type": "digitalocean",
"api_token": "{{user `token`}}",
"image": "ubuntu-20-04-x64",
"region": "fra1",
"size": "s-1vcpu-1gb",
"ssh_username": "root",
"snapshot_name": "{{user `image_name`}}"
}
],
"provisioners": [
{
"type": "shell",
"inline": [
"cloud-init status --wait",
"mkdir -p /opt/simplex"
]
},
{
"type": "file",
"source": "files/opt/",
"destination": "/opt/"
},
{
"type": "shell",
"environment_vars": [
"application_name={{user `application_name`}}",
"DEBIAN_FRONTEND=noninteractive"
],
"scripts": [
"scripts/01-packages.sh",
"scripts/02-firewall.sh",
"scripts/03-init-start.sh",
"scripts/90-cleanup.sh",
"scripts/99-img-check.sh"
]
}
]
}
@@ -0,0 +1,25 @@
#!/bin/sh
# https://superuser.com/questions/1638779/automatic-yess-to-linux-update-upgrade
# https://superuser.com/questions/1412054/non-interactive-apt-upgrade
sudo DEBIAN_FRONTEND=noninteractive \
apt-get \
-o Dpkg::Options::=--force-confold \
-o Dpkg::Options::=--force-confdef \
-y --allow-downgrades --allow-remove-essential --allow-change-held-packages \
update
sudo DEBIAN_FRONTEND=noninteractive \
apt-get \
-o Dpkg::Options::=--force-confold \
-o Dpkg::Options::=--force-confdef \
-y --allow-downgrades --allow-remove-essential --allow-change-held-packages \
dist-upgrade
# TODO install unattended-upgrades; jq is not needed on DigitalOcean
# sudo DEBIAN_FRONTEND=noninteractive \
# apt-get \
# -o Dpkg::Options::=--force-confold \
# -o Dpkg::Options::=--force-confdef \
# -y --allow-downgrades --allow-remove-essential --allow-change-held-packages \
# install jq
@@ -0,0 +1,9 @@
#!/bin/sh
# Add firewall
echo "y" | ufw enable
# Open ports
ufw allow ssh
ufw allow https
ufw allow 5223
@@ -0,0 +1,23 @@
#!/bin/bash
chmod +x /opt/simplex/server_bootstrap.sh
# / Create systemd service for server bootstrap script
cat > /etc/systemd/system/server-bootstrap.service << EOF
[Unit]
Description=Server bootstrap script that downloads and initializes SMP server from the latest release
[Service]
Type=oneshot
ExecStart=/opt/simplex/server_bootstrap.sh
[Install]
WantedBy=multi-user.target
EOF
# Create systemd service for server bootstrap script /
# Start systemd service for server bootstrap script
chmod 644 /etc/systemd/system/server-bootstrap.service
sudo systemctl enable server-bootstrap
sudo systemctl start server-bootstrap
@@ -0,0 +1,60 @@
#!/bin/bash
# DigitalOcean Marketplace Image Validation Tool
# © 2021 DigitalOcean LLC.
# This code is licensed under Apache 2.0 license (see LICENSE.md for details)
set -o errexit
# Ensure /tmp exists and has the proper permissions before
# checking for security updates
# https://github.com/digitalocean/marketplace-partners/issues/94
if [[ ! -d /tmp ]]; then
mkdir /tmp
fi
chmod 1777 /tmp
if [ -n "$(command -v yum)" ]; then
yum update -y
yum clean all
elif [ -n "$(command -v apt-get)" ]; then
apt-get -y update
apt-get -y upgrade
apt-get -y autoremove
apt-get -y autoclean
fi
rm -rf /tmp/* /var/tmp/*
history -c
cat /dev/null > /root/.bash_history
unset HISTFILE
find /var/log -mtime -1 -type f -exec truncate -s 0 {} \;
rm -rf /var/log/*.gz /var/log/*.[0-9] /var/log/*-????????
rm -rf /var/lib/cloud/instances/*
rm -f /root/.ssh/authorized_keys /etc/ssh/*key*
touch /etc/ssh/revoked_keys
chmod 600 /etc/ssh/revoked_keys
# Securely erase the unused portion of the filesystem
GREEN='\033[0;32m'
NC='\033[0m'
printf "\n${GREEN}Writing zeros to the remaining disk space to securely
erase the unused portion of the file system.
Depending on your disk size this may take several minutes.
The secure erase will complete successfully when you see:${NC}
dd: writing to '/zerofile': No space left on device\n
Beginning secure erase now\n"
dd if=/dev/zero of=/zerofile &
PID=$!
while [ -d /proc/$PID ]
do
printf "."
sleep 5
done
sync; rm /zerofile; sync
cat /dev/null > /var/log/lastlog; cat /dev/null > /var/log/wtmp
rm /var/log/auth.log
rm /var/log/kern.log
rm /var/log/ufw.log
@@ -0,0 +1,682 @@
#!/bin/bash
# DigitalOcean Marketplace Image Validation Tool
# © 2021 DigitalOcean LLC.
# This code is licensed under Apache 2.0 license (see LICENSE.md for details)
VERSION="v. 1.6"
RUNDATE=$( date )
# Script should be run with SUDO
if [ "$EUID" -ne 0 ]
then echo "[Error] - This script must be run with sudo or as the root user."
exit 1
fi
STATUS=0
PASS=0
WARN=0
FAIL=0
# $1 == command to check for
# returns: 0 == true, 1 == false
cmdExists() {
if command -v "$1" > /dev/null 2>&1; then
return 0
else
return 1
fi
}
function getDistro {
if [ -f /etc/os-release ]; then
# freedesktop.org and systemd
. /etc/os-release
OS=$NAME
VER=$VERSION_ID
elif type lsb_release >/dev/null 2>&1; then
# linuxbase.org
OS=$(lsb_release -si)
VER=$(lsb_release -sr)
elif [ -f /etc/lsb-release ]; then
# For some versions of Debian/Ubuntu without lsb_release command
. /etc/lsb-release
OS=$DISTRIB_ID
VER=$DISTRIB_RELEASE
elif [ -f /etc/debian_version ]; then
# Older Debian/Ubuntu/etc.
OS=Debian
VER=$(cat /etc/debian_version)
elif [ -f /etc/SuSe-release ]; then
# Older SuSE/etc.
:
elif [ -f /etc/redhat-release ]; then
# Older Red Hat, CentOS, etc.
VER=$( cat /etc/redhat-release | cut -d" " -f3 | cut -d "." -f1)
d=$( cat /etc/redhat-release | cut -d" " -f1 | cut -d "." -f1)
if [[ $d == "CentOS" ]]; then
OS="CentOS Linux"
fi
else
# Fall back to uname, e.g. "Linux <version>", also works for BSD, etc.
OS=$(uname -s)
VER=$(uname -r)
fi
}
function loadPasswords {
SHADOW=$(cat /etc/shadow)
}
function checkAgent {
# Check for the presence of the do-agent in the filesystem
if [ -d /var/opt/digitalocean/do-agent ];then
echo -en "\e[41m[FAIL]\e[0m DigitalOcean Monitoring Agent detected.\n"
((FAIL++))
STATUS=2
if [[ $OS == "CentOS Linux" ]]; then
echo "The agent can be removed with 'sudo yum remove do-agent' "
elif [[ $OS == "Ubuntu" ]]; then
echo "The agent can be removed with 'sudo apt-get purge do-agent' "
fi
else
echo -en "\e[32m[PASS]\e[0m DigitalOcean Monitoring agent was not found\n"
((PASS++))
fi
}
function checkLogs {
cp_ignore="/var/log/cpanel-install.log"
echo -en "\nChecking for log files in /var/log\n\n"
# Check if there are log archives or log files that have not been recently cleared.
for f in /var/log/*-????????; do
[[ -e $f ]] || break
if [ $f != $cp_ignore ]; then
echo -en "\e[93m[WARN]\e[0m Log archive ${f} found\n"
((WARN++))
if [[ $STATUS != 2 ]]; then
STATUS=1
fi
fi
done
for f in /var/log/*.[0-9];do
[[ -e $f ]] || break
echo -en "\e[93m[WARN]\e[0m Log archive ${f} found\n"
((WARN++))
if [[ $STATUS != 2 ]]; then
STATUS=1
fi
done
for f in /var/log/*.log; do
[[ -e $f ]] || break
if [[ "${f}" = '/var/log/lfd.log' && "$( cat "${f}" | egrep -v '/var/log/messages has been reset| Watching /var/log/messages' | wc -c)" -gt 50 ]]; then
if [ $f != $cp_ignore ]; then
echo -en "\e[93m[WARN]\e[0m un-cleared log file, ${f} found\n"
((WARN++))
if [[ $STATUS != 2 ]]; then
STATUS=1
fi
fi
elif [[ "${f}" != '/var/log/lfd.log' && "$( cat "${f}" | wc -c)" -gt 50 ]]; then
if [ $f != $cp_ignore ]; then
echo -en "\e[93m[WARN]\e[0m un-cleared log file, ${f} found\n"
((WARN++))
if [[ $STATUS != 2 ]]; then
STATUS=1
fi
fi
fi
done
}
function checkTMP {
# Check the /tmp directory to ensure it is empty. Warn on any files found.
return 1
}
function checkRoot {
user="root"
uhome="/root"
for usr in $SHADOW
do
IFS=':' read -r -a u <<< "$usr"
if [[ "${u[0]}" == "${user}" ]]; then
if [[ ${u[1]} == "!" ]] || [[ ${u[1]} == "!!" ]] || [[ ${u[1]} == "*" ]]; then
echo -en "\e[32m[PASS]\e[0m User ${user} has no password set.\n"
((PASS++))
else
echo -en "\e[41m[FAIL]\e[0m User ${user} has a password set on their account.\n"
((FAIL++))
STATUS=2
fi
fi
done
if [ -d ${uhome}/ ]; then
if [ -d ${uhome}/.ssh/ ]; then
if ls ${uhome}/.ssh/*> /dev/null 2>&1; then
for key in ${uhome}/.ssh/*
do
if [ "${key}" == "${uhome}/.ssh/authorized_keys" ]; then
if [ "$( cat "${key}" | wc -c)" -gt 50 ]; then
echo -en "\e[41m[FAIL]\e[0m User \e[1m${user}\e[0m has a populated authorized_keys file in \e[93m${key}\e[0m\n"
akey=$(cat ${key})
echo "File Contents:"
echo $akey
echo "--------------"
((FAIL++))
STATUS=2
fi
elif [ "${key}" == "${uhome}/.ssh/id_rsa" ]; then
if [ "$( cat "${key}" | wc -c)" -gt 0 ]; then
echo -en "\e[41m[FAIL]\e[0m User \e[1m${user}\e[0m has a private key file in \e[93m${key}\e[0m\n"
akey=$(cat ${key})
echo "File Contents:"
echo $akey
echo "--------------"
((FAIL++))
STATUS=2
else
echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has empty private key file in \e[93m${key}\e[0m\n"
((WARN++))
if [[ $STATUS != 2 ]]; then
STATUS=1
fi
fi
elif [ "${key}" != "${uhome}/.ssh/known_hosts" ]; then
echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has a file in their .ssh directory at \e[93m${key}\e[0m\n"
((WARN++))
if [[ $STATUS != 2 ]]; then
STATUS=1
fi
else
if [ "$( cat "${key}" | wc -c)" -gt 50 ]; then
echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has a populated known_hosts file in \e[93m${key}\e[0m\n"
((WARN++))
if [[ $STATUS != 2 ]]; then
STATUS=1
fi
fi
fi
done
else
echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m has no SSH keys present\n"
fi
else
echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m does not have an .ssh directory\n"
fi
if [ -f /root/.bash_history ];then
BH_S=$( cat /root/.bash_history | wc -c)
if [[ $BH_S -lt 200 ]]; then
echo -en "\e[32m[PASS]\e[0m ${user}'s Bash History appears to have been cleared\n"
((PASS++))
else
echo -en "\e[41m[FAIL]\e[0m ${user}'s Bash History should be cleared to prevent sensitive information from leaking\n"
((FAIL++))
STATUS=2
fi
return 1;
else
echo -en "\e[32m[PASS]\e[0m The Root User's Bash History is not present\n"
((PASS++))
fi
else
echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m does not have a directory in /home\n"
fi
echo -en "\n\n"
return 1
}
function checkUsers {
# Check each user-created account
for user in $(awk -F: '$3 >= 1000 && $1 != "nobody" {print $1}' /etc/passwd;)
do
# Skip some other non-user system accounts
if [[ $user == "centos" ]]; then
:
elif [[ $user == "nfsnobody" ]]; then
:
else
echo -en "\nChecking user: ${user}...\n"
for usr in $SHADOW
do
IFS=':' read -r -a u <<< "$usr"
if [[ "${u[0]}" == "${user}" ]]; then
if [[ ${u[1]} == "!" ]] || [[ ${u[1]} == "!!" ]] || [[ ${u[1]} == "*" ]]; then
echo -en "\e[32m[PASS]\e[0m User ${user} has no password set.\n"
((PASS++))
else
echo -en "\e[41m[FAIL]\e[0m User ${user} has a password set on their account. Only system users are allowed on the image.\n"
((FAIL++))
STATUS=2
fi
fi
done
#echo "User Found: ${user}"
uhome="/home/${user}"
if [ -d "${uhome}/" ]; then
if [ -d "${uhome}/.ssh/" ]; then
if ls "${uhome}/.ssh/*"> /dev/null 2>&1; then
for key in ${uhome}/.ssh/*
do
if [ "${key}" == "${uhome}/.ssh/authorized_keys" ]; then
if [ "$( cat "${key}" | wc -c)" -gt 50 ]; then
echo -en "\e[41m[FAIL]\e[0m User \e[1m${user}\e[0m has a populated authorized_keys file in \e[93m${key}\e[0m\n"
akey=$(cat ${key})
echo "File Contents:"
echo $akey
echo "--------------"
((FAIL++))
STATUS=2
fi
elif [ "${key}" == "${uhome}/.ssh/id_rsa" ]; then
if [ "$( cat "${key}" | wc -c)" -gt 0 ]; then
echo -en "\e[41m[FAIL]\e[0m User \e[1m${user}\e[0m has a private key file in \e[93m${key}\e[0m\n"
akey=$(cat ${key})
echo "File Contents:"
echo $akey
echo "--------------"
((FAIL++))
STATUS=2
else
echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has empty private key file in \e[93m${key}\e[0m\n"
((WARN++))
if [[ $STATUS != 2 ]]; then
STATUS=1
fi
fi
elif [ "${key}" != "${uhome}/.ssh/known_hosts" ]; then
echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has a file in their .ssh directory named \e[93m${key}\e[0m\n"
((WARN++))
if [[ $STATUS != 2 ]]; then
STATUS=1
fi
else
if [ "$( cat "${key}" | wc -c)" -gt 50 ]; then
echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has a known_hosts file in \e[93m${key}\e[0m\n"
((WARN++))
if [[ $STATUS != 2 ]]; then
STATUS=1
fi
fi
fi
done
else
echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m has no SSH keys present\n"
fi
else
echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m does not have an .ssh directory\n"
fi
else
echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m does not have a directory in /home\n"
fi
# Check for an uncleared .bash_history for this user
if [ -f "${uhome}/.bash_history" ]; then
BH_S=$( cat "${uhome}/.bash_history" | wc -c )
if [[ $BH_S -lt 200 ]]; then
echo -en "\e[32m[PASS]\e[0m ${user}'s Bash History appears to have been cleared\n"
((PASS++))
else
echo -en "\e[41m[FAIL]\e[0m ${user}'s Bash History should be cleared to prevent sensitive information from leaking\n"
((FAIL++))
STATUS=2
fi
echo -en "\n\n"
fi
fi
done
}
function checkFirewall {
if [[ $OS == "Ubuntu" ]]; then
fw="ufw"
ufwa=$(ufw status |head -1| sed -e "s/^Status:\ //")
if [[ $ufwa == "active" ]]; then
FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
((PASS++))
else
FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
((WARN++))
fi
elif [[ $OS == "CentOS Linux" ]]; then
if [ -f /usr/lib/systemd/system/csf.service ]; then
fw="csf"
if [[ $(systemctl status $fw >/dev/null 2>&1) ]]; then
FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
((PASS++))
elif cmdExists "firewall-cmd"; then
if [[ $(systemctl is-active firewalld >/dev/null 2>&1 && echo 1 || echo 0) ]]; then
FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
((PASS++))
else
FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
((WARN++))
fi
else
FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
((WARN++))
fi
else
fw="firewalld"
if [[ $(systemctl is-active firewalld >/dev/null 2>&1 && echo 1 || echo 0) ]]; then
FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
((PASS++))
else
FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
((WARN++))
fi
fi
elif [[ "$OS" =~ Debian.* ]]; then
# user could be using a number of different services for managing their firewall
# we will check some of the most common
if cmdExists 'ufw'; then
fw="ufw"
ufwa=$(ufw status |head -1| sed -e "s/^Status:\ //")
if [[ $ufwa == "active" ]]; then
FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
((PASS++))
else
FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
((WARN++))
fi
elif cmdExists "firewall-cmd"; then
fw="firewalld"
if [[ $(systemctl is-active --quiet $fw) ]]; then
FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
((PASS++))
else
FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
((WARN++))
fi
else
# user could be using vanilla iptables, check if kernel module is loaded
fw="iptables"
if [[ $(lsmod | grep -q '^ip_tables' 2>/dev/null) ]]; then
FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
((PASS++))
else
FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
((WARN++))
fi
fi
fi
}
function checkUpdates {
if [[ $OS == "Ubuntu" ]] || [[ "$OS" =~ Debian.* ]]; then
# Ensure /tmp exists and has the proper permissions before
# checking for security updates
# https://github.com/digitalocean/marketplace-partners/issues/94
if [[ ! -d /tmp ]]; then
mkdir /tmp
fi
chmod 1777 /tmp
echo -en "\nUpdating apt package database to check for security updates, this may take a minute...\n\n"
apt-get -y update > /dev/null
uc=$(apt-get --just-print upgrade | grep -i "security" | wc -l)
if [[ $uc -gt 0 ]]; then
update_count=$(( ${uc} / 2 ))
else
update_count=0
fi
if [[ $update_count -gt 0 ]]; then
echo -en "\e[41m[FAIL]\e[0m There are ${update_count} security updates available for this image that have not been installed.\n"
echo -en
echo -en "Here is a list of the security updates that are not installed:\n"
sleep 2
apt-get --just-print upgrade | grep -i security | awk '{print $2}' | awk '!seen[$0]++'
echo -en
((FAIL++))
STATUS=2
else
echo -en "\e[32m[PASS]\e[0m There are no pending security updates for this image.\n\n"
fi
elif [[ $OS == "CentOS Linux" ]]; then
echo -en "\nChecking for available security updates, this may take a minute...\n\n"
update_count=$(yum check-update --security --quiet | wc -l)
if [[ $update_count -gt 0 ]]; then
echo -en "\e[41m[FAIL]\e[0m There are ${update_count} security updates available for this image that have not been installed.\n"
((FAIL++))
STATUS=2
else
echo -en "\e[32m[PASS]\e[0m There are no pending security updates for this image.\n"
((PASS++))
fi
else
echo "Error encountered"
exit 1
fi
return 1;
}
function checkCloudInit {
if hash cloud-init 2>/dev/null; then
CI="\e[32m[PASS]\e[0m Cloud-init is installed.\n"
((PASS++))
else
CI="\e[41m[FAIL]\e[0m No valid verison of cloud-init was found.\n"
((FAIL++))
STATUS=2
fi
return 1
}
function checkMongoDB {
# Check if MongoDB is installed
# If it is, verify the version is allowed (non-SSPL)
if [[ $OS == "Ubuntu" ]] || [[ "$OS" =~ Debian.* ]]; then
if [[ -f "/usr/bin/mongod" ]]; then
version=$(/usr/bin/mongod --version --quiet | grep "db version" | sed -e "s/^db\ version\ v//")
if version_gt $version 4.0.0; then
if version_gt $version 4.0.3; then
echo -en "\e[41m[FAIL]\e[0m An SSPL version of MongoDB is present, ${version}"
((FAIL++))
STATUS=2
else
echo -en "\e[32m[PASS]\e[0m The version of MongoDB installed, ${version} is not under the SSPL"
((PASS++))
fi
else
if version_gt $version 3.6.8; then
echo -en "\e[41m[FAIL]\e[0m An SSPL version of MongoDB is present, ${version}"
((FAIL++))
STATUS=2
else
echo -en "\e[32m[PASS]\e[0m The version of MongoDB installed, ${version} is not under the SSPL"
((PASS++))
fi
fi
else
echo -en "\e[32m[PASS]\e[0m MongoDB is not installed"
((PASS++))
fi
elif [[ $OS == "CentOS Linux" ]]; then
if [[ -f "/usr/bin/mongod" ]]; then
version=$(/usr/bin/mongod --version --quiet | grep "db version" | sed -e "s/^db\ version\ v//")
if version_gt $version 4.0.0; then
if version_gt $version 4.0.3; then
echo -en "\e[41m[FAIL]\e[0m An SSPL version of MongoDB is present"
((FAIL++))
STATUS=2
else
echo -en "\e[32m[PASS]\e[0m The version of MongoDB installed is not under the SSPL"
((PASS++))
fi
else
if version_gt $version 3.6.8; then
echo -en "\e[41m[FAIL]\e[0m An SSPL version of MongoDB is present"
((FAIL++))
STATUS=2
else
echo -en "\e[32m[PASS]\e[0m The version of MongoDB installed is not under the SSPL"
((PASS++))
fi
fi
else
echo -en "\e[32m[PASS]\e[0m MongoDB is not installed"
((PASS++))
fi
else
echo "ERROR: Unable to identify distribution"
((FAIL++))
STATUS 2
return 1
fi
}
function version_gt() { test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"; }
clear
echo "DigitalOcean Marketplace Image Validation Tool ${VERSION}"
echo "Executed on: ${RUNDATE}"
echo "Checking local system for Marketplace compatibility..."
getDistro
echo -en "\n\e[1mDistribution:\e[0m ${OS}\n"
echo -en "\e[1mVersion:\e[0m ${VER}\n\n"
ost=0
osv=0
if [[ $OS == "Ubuntu" ]]; then
ost=1
if [[ $VER == "20.04" ]]; then
osv=1
elif [[ $VER == "18.04" ]]; then
osv=1
elif [[ $VER == "16.04" ]]; then
osv=1
else
osv=0
fi
elif [[ "$OS" =~ Debian.* ]]; then
ost=1
case "$VER" in
9)
osv=1
;;
10)
osv=1
;;
*)
osv=2
;;
esac
elif [[ $OS == "CentOS Linux" ]]; then
ost=1
if [[ $VER == "8" ]]; then
osv=1
elif [[ $VER == "7" ]]; then
osv=1
elif [[ $VER == "6" ]]; then
osv=1
else
osv=2
fi
else
ost=0
fi
if [[ $ost == 1 ]]; then
echo -en "\e[32m[PASS]\e[0m Supported Operating System Detected: ${OS}\n"
((PASS++))
else
echo -en "\e[41m[FAIL]\e[0m ${OS} is not a supported Operating System\n"
((FAIL++))
STATUS=2
fi
if [[ $osv == 1 ]]; then
echo -en "\e[32m[PASS]\e[0m Supported Release Detected: ${VER}\n"
((PASS++))
elif [[ $ost == 1 ]]; then
echo -en "\e[41m[FAIL]\e[0m ${OS} ${VER} is not a supported Operating System Version\n"
((FAIL++))
STATUS=2
else
echo "Exiting..."
exit 1
fi
checkCloudInit
echo -en "${CI}"
checkFirewall
echo -en "${FW_VER}"
checkUpdates
loadPasswords
checkLogs
echo -en "\n\nChecking all user-created accounts...\n"
checkUsers
echo -en "\n\nChecking the root account...\n"
checkRoot
checkAgent
checkMongoDB
# Summary
echo -en "\n\n---------------------------------------------------------------------------------------------------\n"
if [[ $STATUS == 0 ]]; then
echo -en "Scan Complete.\n\e[32mAll Tests Passed!\e[0m\n"
elif [[ $STATUS == 1 ]]; then
echo -en "Scan Complete. \n\e[93mSome non-critical tests failed. Please review these items.\e[0m\e[0m\n"
else
echo -en "Scan Complete. \n\e[41mOne or more tests failed. Please review these items and re-test.\e[0m\n"
fi
echo "---------------------------------------------------------------------------------------------------"
echo -en "\e[1m${PASS} Tests PASSED\e[0m\n"
echo -en "\e[1m${WARN} WARNINGS\e[0m\n"
echo -en "\e[1m${FAIL} Tests FAILED\e[0m\n"
echo -en "---------------------------------------------------------------------------------------------------\n"
if [[ $STATUS == 0 ]]; then
echo -en "We did not detect any issues with this image. Please be sure to manually ensure that all software installed on the base system is functional, secure and properly configured (or facilities for configuration on first-boot have been created).\n\n"
exit 0
elif [[ $STATUS == 1 ]]; then
echo -en "Please review all [WARN] items above and ensure they are intended or resolved. If you do not have a specific requirement, we recommend resolving these items before image submission\n\n"
exit 0
else
echo -en "Some critical tests failed. These items must be resolved and this scan re-run before you submit your image to the DigitalOcean Marketplace.\n\n"
exit 1
fi
+167
View File
@@ -0,0 +1,167 @@
#!/bin/bash
# <UDF name="enable_store_log" label="Store log - persist SMP queues to append only log and restore them upon server restart." default="on" oneof="on, off" />
# <UDF name="api_token" label="Linode API token - enable Linode to create tags with server address, fingerprint and version. Note: minimal permissions token should have are read/write access to `linodes` (to create tags) and `domains` (to add A record for the third level domain if FQDN is provided)." default="" />
# <UDF name="fqdn" label="FQDN (Fully Qualified Domain Name) - provide third level domain name (e.g. smp.example.com). If provided use `smp://fingerprint@FQDN` as server address in the client. If FQDN is not provided use `smp://fingerprint@IP` instead." default="" />
# Log all stdout output to stackscript.log
exec &> >(tee -i /var/log/stackscript.log)
# Uncomment next line to enable debugging features
# set -xeo pipefail
cd $HOME
# https://superuser.com/questions/1638779/automatic-yess-to-linux-update-upgrade
# https://superuser.com/questions/1412054/non-interactive-apt-upgrade
sudo DEBIAN_FRONTEND=noninteractive \
apt-get \
-o Dpkg::Options::=--force-confold \
-o Dpkg::Options::=--force-confdef \
-y --allow-downgrades --allow-remove-essential --allow-change-held-packages \
update
sudo DEBIAN_FRONTEND=noninteractive \
apt-get \
-o Dpkg::Options::=--force-confold \
-o Dpkg::Options::=--force-confdef \
-y --allow-downgrades --allow-remove-essential --allow-change-held-packages \
dist-upgrade
# TODO install unattended-upgrades
sudo DEBIAN_FRONTEND=noninteractive \
apt-get \
-o Dpkg::Options::=--force-confold \
-o Dpkg::Options::=--force-confdef \
-y --allow-downgrades --allow-remove-essential --allow-change-held-packages \
install jq
# Add firewall
echo "y" | ufw enable
# Open ports
ufw allow ssh
ufw allow https
ufw allow 5223
# Download latest release
bin_dir="/opt/simplex/bin"
binary="$bin_dir/smp-server"
mkdir -p $bin_dir
curl -L -o $binary https://github.com/simplex-chat/simplexmq/releases/latest/download/smp-server-ubuntu-20_04-x86-64
chmod +x $binary
# / Add to PATH
cat > /etc/profile.d/simplex.sh << EOF
#!/bin/bash
export PATH="$PATH:$bin_dir"
EOF
# Add to PATH /
# Source and test PATH
source /etc/profile.d/simplex.sh
smp-server --version
# Initialize server
init_opts=()
[[ $ENABLE_STORE_LOG == "on" ]] && init_opts+=(-l)
ip_address=$(curl ifconfig.me)
init_opts+=(--ip $ip_address)
[[ -n "$FQDN" ]] && init_opts+=(-n $FQDN)
smp-server init "${init_opts[@]}"
# Server fingerprint
fingerprint=$(cat /etc/opt/simplex/fingerprint)
# Determine server address to specify in welcome script and Linode tag
# ! If FQDN was provided and used as part of server initialization, server's certificate will not pass validation at client
# ! if client tries to connect by server's IP address, so we have to specify FQDN as server address in Linode tag and
# ! in welcome script regardless of creation of A record in Linode
# ! https://hackage.haskell.org/package/x509-validation-1.6.10/docs/src/Data-X509-Validation.html#validateCertificateName
if [[ -n "$FQDN" ]]; then
server_address=$FQDN
else
server_address=$ip_address
fi
# Set up welcome script
on_login_script="/opt/simplex/on_login.sh"
# / Welcome script
cat > $on_login_script << EOF
#!/bin/bash
fingerprint=\$1
server_address=\$2
cat << EOF2
********************************************************************************
SMP server address: smp://\$fingerprint@\$server_address
Check SMP server status with: systemctl status smp-server
To keep this server secure, the UFW firewall is enabled.
All ports are BLOCKED except 22 (SSH), 443 (HTTPS), 5223 (SMP server).
********************************************************************************
To stop seeing this message delete line - bash /opt/simplex/on_login.sh - from /root/.bashrc
EOF2
EOF
# Welcome script /
chmod +x $on_login_script
echo "bash $on_login_script $fingerprint $server_address" >> /root/.bashrc
# Create A record and update Linode's tags
if [[ -n "$API_TOKEN" ]]; then
if [[ -n "$FQDN" ]]; then
domain_address=$(echo $FQDN | rev | cut -d "." -f 1,2 | rev)
domain_id=$(curl -H "Authorization: Bearer $API_TOKEN" https://api.linode.com/v4/domains \
| jq --arg da "$domain_address" '.data[] | select( .domain == $da ) | .id')
if [[ -n $domain_id ]]; then
curl \
-s -H "Content-Type: application/json" \
-H "Authorization: Bearer $API_TOKEN" \
-X POST -d "{\"type\":\"A\",\"name\":\"$FQDN\",\"target\":\"$ip_address\"}" \
https://api.linode.com/v4/domains/${domain_id}/records
fi
fi
version=$(smp-server --version | cut -d ' ' -f 3-)
curl \
-s -H "Content-Type: application/json" \
-H "Authorization: Bearer $API_TOKEN" \
-X PUT -d "{\"tags\":[\"$server_address\",\"$fingerprint\",\"$version\"]}" \
https://api.linode.com/v4/linode/instances/$LINODE_ID
fi
# / Create systemd service
cat > /etc/systemd/system/smp-server.service << EOF
[Unit]
Description=SMP server
[Service]
Type=simple
ExecStart=/bin/sh -c "exec $binary start >> /var/opt/simplex/smp-server.log 2>&1"
[Install]
WantedBy=multi-user.target
EOF
# Create systemd service /
# Start systemd service
chmod 644 /etc/systemd/system/smp-server.service
sudo systemctl enable smp-server
sudo systemctl start smp-server
# Reboot Linode to apply upgrades
sudo reboot
+285
View File
@@ -0,0 +1,285 @@
cabal-version: 1.12
-- This file has been generated from package.yaml by hpack version 0.34.4.
--
-- see: https://github.com/sol/hpack
name: simplexmq
version: 1.0.3
synopsis: SimpleXMQ message broker
description: This package includes <./docs/Simplex-Messaging-Server.html server>,
<./docs/Simplex-Messaging-Client.html client> and
<./docs/Simplex-Messaging-Agent.html agent> for SMP protocols:
.
* <https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md SMP protocol>
* <https://github.com/simplex-chat/simplexmq/blob/master/protocol/agent-protocol.md SMP agent protocol>
.
See <https://github.com/simplex-chat/simplex-chat terminal chat prototype> built with SimpleXMQ broker.
category: Chat, Network, Web, System, Cryptography
homepage: https://github.com/simplex-chat/simplexmq#readme
author: simplex.chat
maintainer: chat@simplex.chat
copyright: 2020-2022 simplex.chat
license: AGPL-3
license-file: LICENSE
build-type: Simple
extra-source-files:
README.md
CHANGELOG.md
library
exposed-modules:
Simplex.Messaging.Agent
Simplex.Messaging.Agent.Client
Simplex.Messaging.Agent.Env.SQLite
Simplex.Messaging.Agent.Protocol
Simplex.Messaging.Agent.QueryString
Simplex.Messaging.Agent.RetryInterval
Simplex.Messaging.Agent.Server
Simplex.Messaging.Agent.Store
Simplex.Messaging.Agent.Store.SQLite
Simplex.Messaging.Agent.Store.SQLite.Migrations
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220101_initial
Simplex.Messaging.Client
Simplex.Messaging.Crypto
Simplex.Messaging.Crypto.Ratchet
Simplex.Messaging.Encoding
Simplex.Messaging.Encoding.String
Simplex.Messaging.Parsers
Simplex.Messaging.Protocol
Simplex.Messaging.Server
Simplex.Messaging.Server.Env.STM
Simplex.Messaging.Server.MsgStore
Simplex.Messaging.Server.MsgStore.STM
Simplex.Messaging.Server.QueueStore
Simplex.Messaging.Server.QueueStore.STM
Simplex.Messaging.Server.StoreLog
Simplex.Messaging.Transport
Simplex.Messaging.Transport.Client
Simplex.Messaging.Transport.Server
Simplex.Messaging.Transport.WebSockets
Simplex.Messaging.Util
Simplex.Messaging.Version
other-modules:
Paths_simplexmq
hs-source-dirs:
src
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns
build-depends:
QuickCheck ==2.14.*
, aeson ==2.0.*
, ansi-terminal >=0.10 && <0.12
, asn1-encoding ==0.9.*
, asn1-types ==0.3.*
, async ==2.2.*
, attoparsec ==0.14.*
, base >=4.7 && <5
, base64-bytestring >=1.0 && <1.3
, bytestring ==0.10.*
, composition ==1.0.*
, constraints >=0.12 && <0.14
, containers ==0.6.*
, cryptonite >=0.27 && <0.30
, cryptostore ==0.2.*
, data-default ==0.7.*
, direct-sqlite ==2.3.*
, directory ==1.3.*
, filepath ==1.4.*
, generic-random >=1.3 && <1.5
, http-types ==0.12.*
, iso8601-time ==0.1.*
, memory ==0.15.*
, mtl ==2.2.*
, network ==3.1.*
, network-transport ==0.5.*
, random >=1.1 && <1.3
, simple-logger ==0.1.*
, sqlite-simple ==0.4.*
, stm ==2.5.*
, template-haskell ==2.16.*
, text ==1.2.*
, time ==1.9.*
, tls >=1.5.7 && <1.6
, transformers ==0.5.*
, unliftio ==0.2.*
, unliftio-core ==0.2.*
, websockets ==0.12.*
, x509 ==1.7.*
, x509-store ==1.6.*
, x509-validation ==1.6.*
default-language: Haskell2010
executable smp-agent
main-is: Main.hs
other-modules:
Paths_simplexmq
hs-source-dirs:
apps/smp-agent
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded
build-depends:
QuickCheck ==2.14.*
, aeson ==2.0.*
, ansi-terminal >=0.10 && <0.12
, asn1-encoding ==0.9.*
, asn1-types ==0.3.*
, async ==2.2.*
, attoparsec ==0.14.*
, base >=4.7 && <5
, base64-bytestring >=1.0 && <1.3
, bytestring ==0.10.*
, composition ==1.0.*
, constraints >=0.12 && <0.14
, containers ==0.6.*
, cryptonite >=0.27 && <0.30
, cryptostore ==0.2.*
, data-default ==0.7.*
, direct-sqlite ==2.3.*
, directory ==1.3.*
, filepath ==1.4.*
, generic-random >=1.3 && <1.5
, http-types ==0.12.*
, iso8601-time ==0.1.*
, memory ==0.15.*
, mtl ==2.2.*
, network ==3.1.*
, network-transport ==0.5.*
, random >=1.1 && <1.3
, simple-logger ==0.1.*
, simplexmq
, sqlite-simple ==0.4.*
, stm ==2.5.*
, template-haskell ==2.16.*
, text ==1.2.*
, time ==1.9.*
, tls >=1.5.7 && <1.6
, transformers ==0.5.*
, unliftio ==0.2.*
, unliftio-core ==0.2.*
, websockets ==0.12.*
, x509 ==1.7.*
, x509-store ==1.6.*
, x509-validation ==1.6.*
default-language: Haskell2010
executable smp-server
main-is: Main.hs
other-modules:
Paths_simplexmq
hs-source-dirs:
apps/smp-server
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded
build-depends:
QuickCheck ==2.14.*
, aeson ==2.0.*
, ansi-terminal >=0.10 && <0.12
, asn1-encoding ==0.9.*
, asn1-types ==0.3.*
, async ==2.2.*
, attoparsec ==0.14.*
, base >=4.7 && <5
, base64-bytestring >=1.0 && <1.3
, bytestring ==0.10.*
, composition ==1.0.*
, constraints >=0.12 && <0.14
, containers ==0.6.*
, cryptonite >=0.27 && <0.30
, cryptostore ==0.2.*
, data-default ==0.7.*
, direct-sqlite ==2.3.*
, directory ==1.3.*
, filepath ==1.4.*
, generic-random >=1.3 && <1.5
, http-types ==0.12.*
, ini ==0.4.*
, iso8601-time ==0.1.*
, memory ==0.15.*
, mtl ==2.2.*
, network ==3.1.*
, network-transport ==0.5.*
, optparse-applicative >=0.15 && <0.17
, process ==1.6.*
, random >=1.1 && <1.3
, simple-logger ==0.1.*
, simplexmq
, sqlite-simple ==0.4.*
, stm ==2.5.*
, template-haskell ==2.16.*
, text ==1.2.*
, time ==1.9.*
, tls >=1.5.7 && <1.6
, transformers ==0.5.*
, unliftio ==0.2.*
, unliftio-core ==0.2.*
, websockets ==0.12.*
, x509 ==1.7.*
, x509-store ==1.6.*
, x509-validation ==1.6.*
default-language: Haskell2010
test-suite smp-server-test
type: exitcode-stdio-1.0
main-is: Test.hs
other-modules:
AgentTests
AgentTests.ConnectionRequestTests
AgentTests.DoubleRatchetTests
AgentTests.FunctionalAPITests
AgentTests.SQLiteTests
CoreTests.EncodingTests
CoreTests.ProtocolErrorTests
CoreTests.VersionRangeTests
ServerTests
SMPAgentClient
SMPClient
Paths_simplexmq
hs-source-dirs:
tests
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns
build-depends:
HUnit ==1.6.*
, QuickCheck ==2.14.*
, aeson ==2.0.*
, ansi-terminal >=0.10 && <0.12
, asn1-encoding ==0.9.*
, asn1-types ==0.3.*
, async ==2.2.*
, attoparsec ==0.14.*
, base >=4.7 && <5
, base64-bytestring >=1.0 && <1.3
, bytestring ==0.10.*
, composition ==1.0.*
, constraints >=0.12 && <0.14
, containers ==0.6.*
, cryptonite >=0.27 && <0.30
, cryptostore ==0.2.*
, data-default ==0.7.*
, direct-sqlite ==2.3.*
, directory ==1.3.*
, filepath ==1.4.*
, generic-random >=1.3 && <1.5
, hspec ==2.7.*
, hspec-core ==2.7.*
, http-types ==0.12.*
, iso8601-time ==0.1.*
, memory ==0.15.*
, mtl ==2.2.*
, network ==3.1.*
, network-transport ==0.5.*
, random >=1.1 && <1.3
, simple-logger ==0.1.*
, simplexmq
, sqlite-simple ==0.4.*
, stm ==2.5.*
, template-haskell ==2.16.*
, text ==1.2.*
, time ==1.9.*
, timeit ==2.0.*
, tls >=1.5.7 && <1.6
, transformers ==0.5.*
, unliftio ==0.2.*
, unliftio-core ==0.2.*
, websockets ==0.12.*
, x509 ==1.7.*
, x509-store ==1.6.*
, x509-validation ==1.6.*
default-language: Haskell2010
+568 -276
View File
@@ -1,12 +1,17 @@
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
-- |
-- Module : Simplex.Messaging.Agent
@@ -21,10 +26,28 @@
--
-- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/agent-protocol.md
module Simplex.Messaging.Agent
( runSMPAgent,
runSMPAgentBlocking,
( -- * queue-based SMP agent
getAgentClient,
runAgentClient,
-- * SMP agent functional API
AgentClient (..),
AgentMonad,
AgentErrorMonad,
getSMPAgentClient,
runSMPAgentClient,
disconnectAgentClient, -- used in tests
withAgentLock,
createConnection,
joinConnection,
allowConnection,
acceptContact,
rejectContact,
subscribeConnection,
sendMessage,
ackMessage,
suspendConnection,
deleteConnection,
logConnection,
)
where
@@ -34,62 +57,101 @@ import Control.Monad.Except
import Control.Monad.IO.Unlift (MonadUnliftIO)
import Control.Monad.Reader
import Crypto.Random (MonadRandom)
import Data.Bifunctor (first, second)
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Composition ((.:), (.:.))
import Data.Functor (($>))
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as L
import qualified Data.Map.Strict as M
import Data.Maybe (isJust)
import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8)
import Data.Time.Clock
import Data.Time.Clock.System (systemToUTCTime)
import Database.SQLite.Simple (SQLError)
import Simplex.Messaging.Agent.Client
import Simplex.Messaging.Agent.Env.SQLite
import Simplex.Messaging.Agent.Protocol
import Simplex.Messaging.Agent.RetryInterval
import Simplex.Messaging.Agent.Store
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore, connectSQLiteStore)
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore)
import Simplex.Messaging.Client (SMPServerTransmission)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Protocol (CorrId (..), MsgBody, SenderPublicKey)
import qualified Simplex.Messaging.Crypto.Ratchet as CR
import Simplex.Messaging.Encoding
import Simplex.Messaging.Parsers (parse)
import Simplex.Messaging.Protocol (MsgBody)
import qualified Simplex.Messaging.Protocol as SMP
import Simplex.Messaging.Transport (ATransport (..), TProxy, Transport (..), runTransportServer)
import Simplex.Messaging.Util (bshow)
import Simplex.Messaging.Util (bshow, liftError, tryError, unlessM)
import Simplex.Messaging.Version
import System.Random (randomR)
import UnliftIO.Async (race_)
import UnliftIO.Async (async, race_)
import qualified UnliftIO.Exception as E
import UnliftIO.STM
-- | Runs an SMP agent as a TCP service using passed configuration.
--
-- See a full agent executable here: https://github.com/simplex-chat/simplexmq/blob/master/apps/smp-agent/Main.hs
runSMPAgent :: (MonadRandom m, MonadUnliftIO m) => ATransport -> AgentConfig -> m ()
runSMPAgent t cfg = do
started <- newEmptyTMVarIO
runSMPAgentBlocking t started cfg
-- | Runs an SMP agent as a TCP service using passed configuration with signalling.
--
-- This function uses passed TMVar to signal when the server is ready to accept TCP requests (True)
-- and when it is disconnected from the TCP socket once the server thread is killed (False).
runSMPAgentBlocking :: (MonadRandom m, MonadUnliftIO m) => ATransport -> TMVar Bool -> AgentConfig -> m ()
runSMPAgentBlocking (ATransport t) started cfg@AgentConfig {tcpPort} = runReaderT (smpAgent t) =<< newSMPAgentEnv cfg
-- | Creates an SMP agent client instance
getSMPAgentClient :: (MonadRandom m, MonadUnliftIO m) => AgentConfig -> m AgentClient
getSMPAgentClient cfg = newSMPAgentEnv cfg >>= runReaderT runAgent
where
smpAgent :: forall c m'. (Transport c, MonadUnliftIO m', MonadReader Env m') => TProxy c -> m' ()
smpAgent _ = runTransportServer started tcpPort $ \(h :: c) -> do
liftIO $ putLn h "Welcome to SMP v0.3.2 agent"
c <- getSMPAgentClient
logConnection c True
race_ (connectClient h c) (runSMPAgentClient c)
`E.finally` (closeSMPServerClients c >> logConnection c False)
runAgent = do
c <- getAgentClient
action <- async $ subscriber c `E.finally` disconnectAgentClient c
pure c {smpSubscriber = action}
-- | Creates an SMP agent instance that receives commands and sends responses via 'TBQueue's.
getSMPAgentClient :: (MonadUnliftIO m, MonadReader Env m) => m AgentClient
getSMPAgentClient = do
n <- asks clientCounter
cfg <- asks config
atomically $ newAgentClient n cfg
disconnectAgentClient :: MonadUnliftIO m => AgentClient -> m ()
disconnectAgentClient c = closeAgentClient c >> logConnection c False
connectClient :: Transport c => MonadUnliftIO m => c -> AgentClient -> m ()
connectClient h c = race_ (send h c) (receive h c)
-- |
type AgentErrorMonad m = (MonadUnliftIO m, MonadError AgentErrorType m)
-- | Create SMP agent connection (NEW command)
createConnection :: AgentErrorMonad m => AgentClient -> SConnectionMode c -> m (ConnId, ConnectionRequestUri c)
createConnection c cMode = withAgentEnv c $ newConn c "" cMode
-- | Join SMP agent connection (JOIN command)
joinConnection :: AgentErrorMonad m => AgentClient -> ConnectionRequestUri c -> ConnInfo -> m ConnId
joinConnection c = withAgentEnv c .: joinConn c ""
-- | Allow connection to continue after CONF notification (LET command)
allowConnection :: AgentErrorMonad m => AgentClient -> ConnId -> ConfirmationId -> ConnInfo -> m ()
allowConnection c = withAgentEnv c .:. allowConnection' c
-- | Accept contact after REQ notification (ACPT command)
acceptContact :: AgentErrorMonad m => AgentClient -> ConfirmationId -> ConnInfo -> m ConnId
acceptContact c = withAgentEnv c .: acceptContact' c ""
-- | Reject contact (RJCT command)
rejectContact :: AgentErrorMonad m => AgentClient -> ConnId -> ConfirmationId -> m ()
rejectContact c = withAgentEnv c .: rejectContact' c
-- | Subscribe to receive connection messages (SUB command)
subscribeConnection :: AgentErrorMonad m => AgentClient -> ConnId -> m ()
subscribeConnection c = withAgentEnv c . subscribeConnection' c
-- | Send message to the connection (SEND command)
sendMessage :: AgentErrorMonad m => AgentClient -> ConnId -> MsgBody -> m AgentMsgId
sendMessage c = withAgentEnv c .: sendMessage' c
ackMessage :: AgentErrorMonad m => AgentClient -> ConnId -> AgentMsgId -> m ()
ackMessage c = withAgentEnv c .: ackMessage' c
-- | Suspend SMP agent connection (OFF command)
suspendConnection :: AgentErrorMonad m => AgentClient -> ConnId -> m ()
suspendConnection c = withAgentEnv c . suspendConnection' c
-- | Delete SMP agent connection (DEL command)
deleteConnection :: AgentErrorMonad m => AgentClient -> ConnId -> m ()
deleteConnection c = withAgentEnv c . deleteConnection' c
withAgentEnv :: AgentClient -> ReaderT Env m a -> m a
withAgentEnv c = (`runReaderT` agentEnv c)
-- withAgentClient :: AgentErrorMonad m => AgentClient -> ReaderT Env m a -> m a
-- withAgentClient c = withAgentLock c . withAgentEnv c
-- | Creates an SMP agent client instance that receives commands and sends responses via 'TBQueue's.
getAgentClient :: (MonadUnliftIO m, MonadReader Env m) => m AgentClient
getAgentClient = ask >>= atomically . newAgentClient
logConnection :: MonadUnliftIO m => AgentClient -> Bool -> m ()
logConnection c connected =
@@ -97,213 +159,378 @@ logConnection c connected =
in logInfo $ T.unwords ["client", showText (clientId c), event, "Agent"]
-- | Runs an SMP agent instance that receives commands and sends responses via 'TBQueue's.
runSMPAgentClient :: (MonadUnliftIO m, MonadReader Env m) => AgentClient -> m ()
runSMPAgentClient c = do
db <- asks $ dbFile . config
s1 <- connectSQLiteStore db
s2 <- connectSQLiteStore db
race_ (subscriber c s1) (client c s2)
runAgentClient :: (MonadUnliftIO m, MonadReader Env m) => AgentClient -> m ()
runAgentClient c = race_ (subscriber c) (client c)
receive :: forall c m. (Transport c, MonadUnliftIO m) => c -> AgentClient -> m ()
receive h c@AgentClient {rcvQ, sndQ} = forever $ do
(corrId, cAlias, cmdOrErr) <- tGet SClient h
case cmdOrErr of
Right cmd -> write rcvQ (corrId, cAlias, cmd)
Left e -> write sndQ (corrId, cAlias, ERR e)
where
write :: TBQueue (ATransmission p) -> ATransmission p -> m ()
write q t = do
logClient c "-->" t
atomically $ writeTBQueue q t
send :: (Transport c, MonadUnliftIO m) => c -> AgentClient -> m ()
send h c@AgentClient {sndQ} = forever $ do
t <- atomically $ readTBQueue sndQ
tPut h t
logClient c "<--" t
logClient :: MonadUnliftIO m => AgentClient -> ByteString -> ATransmission a -> m ()
logClient AgentClient {clientId} dir (CorrId corrId, cAlias, cmd) = do
logInfo . decodeUtf8 $ B.unwords [bshow clientId, dir, "A :", corrId, cAlias, B.takeWhile (/= ' ') $ serializeCommand cmd]
client :: (MonadUnliftIO m, MonadReader Env m) => AgentClient -> SQLiteStore -> m ()
client c@AgentClient {rcvQ, sndQ} st = forever $ do
t@(corrId, cAlias, _) <- atomically $ readTBQueue rcvQ
runExceptT (processCommand c st t) >>= \case
Left e -> atomically $ writeTBQueue sndQ (corrId, cAlias, ERR e)
Right _ -> return ()
client :: forall m. (MonadUnliftIO m, MonadReader Env m) => AgentClient -> m ()
client c@AgentClient {rcvQ, subQ} = forever $ do
(corrId, connId, cmd) <- atomically $ readTBQueue rcvQ
withAgentLock c (runExceptT $ processCommand c (connId, cmd))
>>= atomically . writeTBQueue subQ . \case
Left e -> (corrId, connId, ERR e)
Right (connId', resp) -> (corrId, connId', resp)
withStore ::
AgentMonad m =>
(forall m'. (MonadUnliftIO m', MonadError StoreError m') => m' a) ->
(forall m'. (MonadUnliftIO m', MonadError StoreError m') => SQLiteStore -> m' a) ->
m a
withStore action = do
runExceptT (action `E.catch` handleInternal) >>= \case
st <- asks store
runExceptT (action st `E.catch` handleInternal) >>= \case
Right c -> return c
Left e -> throwError $ storeError e
where
-- TODO when parsing exception happens in store, the agent hangs;
-- changing SQLError to SomeException does not help
handleInternal :: (MonadError StoreError m') => SQLError -> m' a
handleInternal e = throwError . SEInternal $ bshow e
storeError :: StoreError -> AgentErrorType
storeError = \case
SEConnNotFound -> CONN UNKNOWN
SEConnNotFound -> CONN NOT_FOUND
SEConnDuplicate -> CONN DUPLICATE
SEBadConnType CRcv -> CONN SIMPLEX
SEBadConnType CSnd -> CONN SIMPLEX
SEInvitationNotFound -> CMD PROHIBITED
e -> INTERNAL $ show e
processCommand :: forall m. AgentMonad m => AgentClient -> SQLiteStore -> ATransmission 'Client -> m ()
processCommand c@AgentClient {sndQ} st (corrId, connAlias, cmd) =
case cmd of
NEW -> createNewConnection
JOIN smpQueueInfo replyMode -> joinConnection smpQueueInfo replyMode
SUB -> subscribeConnection connAlias
SUBALL -> subscribeAll
SEND msgBody -> sendMessage msgBody
OFF -> suspendConnection
DEL -> deleteConnection
-- | execute any SMP agent command
processCommand :: forall m. AgentMonad m => AgentClient -> (ConnId, ACommand 'Client) -> m (ConnId, ACommand 'Agent)
processCommand c (connId, cmd) = case cmd of
NEW (ACM cMode) -> second (INV . ACR cMode) <$> newConn c connId cMode
JOIN (ACR _ cReq) connInfo -> (,OK) <$> joinConn c connId cReq connInfo
LET confId ownCInfo -> allowConnection' c connId confId ownCInfo $> (connId, OK)
ACPT invId ownCInfo -> (,OK) <$> acceptContact' c connId invId ownCInfo
RJCT invId -> rejectContact' c connId invId $> (connId, OK)
SUB -> subscribeConnection' c connId $> (connId, OK)
SEND msgBody -> (connId,) . MID <$> sendMessage' c connId msgBody
ACK msgId -> ackMessage' c connId msgId $> (connId, OK)
OFF -> suspendConnection' c connId $> (connId, OK)
DEL -> deleteConnection' c connId $> (connId, OK)
newConn :: AgentMonad m => AgentClient -> ConnId -> SConnectionMode c -> m (ConnId, ConnectionRequestUri c)
newConn c connId cMode = do
srv <- getSMPServer
(rq, qUri) <- newRcvQueue c srv
g <- asks idsDrg
let cData = ConnData {connId}
connId' <- withStore $ \st -> createRcvConn st g cData rq cMode
addSubscription c rq connId'
let crData = ConnReqUriData simplexChat smpAgentVRange [qUri]
case cMode of
SCMContact -> pure (connId', CRContactUri crData)
SCMInvitation -> do
(pk1, pk2, e2eRcvParams) <- liftIO $ CR.generateE2EParams CR.e2eEncryptVersion
withStore $ \st -> createRatchetX3dhKeys st connId' pk1 pk2
pure (connId', CRInvitationUri crData $ toVersionRangeT e2eRcvParams CR.e2eEncryptVRange)
joinConn :: AgentMonad m => AgentClient -> ConnId -> ConnectionRequestUri c -> ConnInfo -> m ConnId
joinConn c connId (CRInvitationUri (ConnReqUriData _ agentVRange (qUri :| _)) e2eRcvParamsUri) cInfo =
case ( qUri `compatibleVersion` SMP.smpClientVRange,
e2eRcvParamsUri `compatibleVersion` CR.e2eEncryptVRange,
agentVRange `compatibleVersion` smpAgentVRange
) of
(Just qInfo, Just (Compatible e2eRcvParams@(CR.E2ERatchetParams _ _ rcDHRr)), Just _) -> do
-- TODO in agent v2 - use found compatible version rather than current
(pk1, pk2, e2eSndParams) <- liftIO . CR.generateE2EParams $ version e2eRcvParams
(_, rcDHRs) <- liftIO C.generateKeyPair'
let rc = CR.initSndRatchet rcDHRr rcDHRs $ CR.x3dhSnd pk1 pk2 e2eRcvParams
(sq, smpConf) <- newSndQueue qInfo cInfo
g <- asks idsDrg
let cData = ConnData {connId}
connId' <- withStore $ \st -> do
connId' <- createSndConn st g cData sq
createRatchet st connId' rc
pure connId'
confirmQueue c connId' sq smpConf $ Just e2eSndParams
void $ enqueueMessage c connId' sq HELLO
pure connId'
_ -> throwError $ AGENT A_VERSION
joinConn c connId (CRContactUri (ConnReqUriData _ agentVRange (qUri :| _))) cInfo =
case ( qUri `compatibleVersion` SMP.smpClientVRange,
agentVRange `compatibleVersion` smpAgentVRange
) of
(Just qInfo, Just _) -> do
-- TODO in agent v2 - use found compatible version rather than current
(connId', cReq) <- newConn c connId SCMInvitation
sendInvitation c qInfo cReq cInfo
pure connId'
_ -> throwError $ AGENT A_VERSION
createReplyQueue :: AgentMonad m => AgentClient -> ConnId -> SndQueue -> m ()
createReplyQueue c connId sq = do
srv <- getSMPServer
(rq, qUri) <- newRcvQueue c srv
-- TODO reply queue version should be the same as send queue, ignoring it in v1
let qInfo = toVersionT qUri SMP.smpClientVersion
addSubscription c rq connId
withStore $ \st -> upgradeSndConnToDuplex st connId rq
void . enqueueMessage c connId sq $ REPLY [qInfo]
-- | Approve confirmation (LET command) in Reader monad
allowConnection' :: AgentMonad m => AgentClient -> ConnId -> ConfirmationId -> ConnInfo -> m ()
allowConnection' c connId confId ownConnInfo = do
withStore (`getConn` connId) >>= \case
SomeConn _ (RcvConnection _ rq) -> do
AcceptedConfirmation {senderConf, ratchetState} <- withStore $ \st -> acceptConfirmation st confId ownConnInfo
withStore $ \st -> createRatchet st connId ratchetState
processConfirmation c rq senderConf
_ -> throwError $ CMD PROHIBITED
-- | Accept contact (ACPT command) in Reader monad
acceptContact' :: AgentMonad m => AgentClient -> ConnId -> InvitationId -> ConnInfo -> m ConnId
acceptContact' c connId invId ownConnInfo = do
Invitation {contactConnId, connReq} <- withStore (`getInvitation` invId)
withStore (`getConn` contactConnId) >>= \case
SomeConn _ ContactConnection {} -> do
withStore $ \st -> acceptInvitation st invId ownConnInfo
joinConn c connId connReq ownConnInfo
_ -> throwError $ CMD PROHIBITED
-- | Reject contact (RJCT command) in Reader monad
rejectContact' :: AgentMonad m => AgentClient -> ConnId -> InvitationId -> m ()
rejectContact' _ contactConnId invId =
withStore $ \st -> deleteInvitation st contactConnId invId
processConfirmation :: AgentMonad m => AgentClient -> RcvQueue -> SMPConfirmation -> m ()
processConfirmation c rq@RcvQueue {e2ePrivKey} SMPConfirmation {senderKey, e2ePubKey} = do
let dhSecret = C.dh' e2ePubKey e2ePrivKey
withStore $ \st -> setRcvQueueConfirmedE2E st rq dhSecret
secureQueue c rq senderKey
withStore $ \st -> setRcvQueueStatus st rq Secured
-- | Subscribe to receive connection messages (SUB command) in Reader monad
subscribeConnection' :: forall m. AgentMonad m => AgentClient -> ConnId -> m ()
subscribeConnection' c connId =
withStore (`getConn` connId) >>= \case
SomeConn _ (DuplexConnection _ rq sq) -> do
resumeMsgDelivery c connId sq
subscribeQueue c rq connId
SomeConn _ (SndConnection _ sq) -> do
resumeMsgDelivery c connId sq
case status (sq :: SndQueue) of
Confirmed -> pure ()
Active -> throwError $ CONN SIMPLEX
_ -> throwError $ INTERNAL "unexpected queue status"
SomeConn _ (RcvConnection _ rq) -> subscribeQueue c rq connId
SomeConn _ (ContactConnection _ rq) -> subscribeQueue c rq connId
-- | Send message to the connection (SEND command) in Reader monad
sendMessage' :: forall m. AgentMonad m => AgentClient -> ConnId -> MsgBody -> m AgentMsgId
sendMessage' c connId msg =
withStore (`getConn` connId) >>= \case
SomeConn _ (DuplexConnection _ _ sq) -> enqueueMsg sq
SomeConn _ (SndConnection _ sq) -> enqueueMsg sq
_ -> throwError $ CONN SIMPLEX
where
createNewConnection :: m ()
createNewConnection = do
-- TODO create connection alias if not passed
-- make connAlias Maybe?
srv <- getSMPServer
(rq, qInfo) <- newReceiveQueue c srv connAlias
withStore $ createRcvConn st rq
respond $ INV qInfo
enqueueMsg :: SndQueue -> m AgentMsgId
enqueueMsg sq = enqueueMessage c connId sq $ A_MSG msg
getSMPServer :: m SMPServer
getSMPServer =
asks (smpServers . config) >>= \case
srv :| [] -> pure srv
servers -> do
gen <- asks randomServer
i <- atomically . stateTVar gen $ randomR (0, L.length servers - 1)
pure $ servers L.!! i
enqueueMessage :: forall m. AgentMonad m => AgentClient -> ConnId -> SndQueue -> AMessage -> m AgentMsgId
enqueueMessage c connId sq aMessage = do
resumeMsgDelivery c connId sq
msgId <- storeSentMsg
queuePendingMsgs c connId sq [msgId]
pure $ unId msgId
where
storeSentMsg :: m InternalId
storeSentMsg = do
internalTs <- liftIO getCurrentTime
(internalId, internalSndId, prevMsgHash) <- withStore (`updateSndIds` connId)
let privHeader = APrivHeader (unSndId internalSndId) prevMsgHash
agentMessage = smpEncode $ AgentMessage privHeader aMessage
internalHash = C.sha256Hash agentMessage
joinConnection :: SMPQueueInfo -> ReplyMode -> m ()
joinConnection qInfo (ReplyMode replyMode) = do
-- TODO create connection alias if not passed
-- make connAlias Maybe?
(sq, senderKey, verifyKey) <- newSendQueue qInfo connAlias
withStore $ createSndConn st sq
connectToSendQueue c st sq senderKey verifyKey
when (replyMode == On) $ createReplyQueue sq
-- TODO this response is disabled to avoid two responses in terminal client (OK + CON),
-- respond OK
encAgentMessage <- agentRatchetEncrypt connId agentMessage e2eEncUserMsgLength
let msgBody = smpEncode $ AgentMsgEnvelope {agentVersion = smpAgentVersion, encAgentMessage}
msgType = aMessageType aMessage
msgData = SndMsgData {internalId, internalSndId, internalTs, msgType, msgBody, internalHash, prevMsgHash}
withStore $ \st -> createSndMsg st connId msgData
pure internalId
subscribeConnection :: ConnAlias -> m ()
subscribeConnection cAlias =
withStore (getConn st cAlias) >>= \case
SomeConn _ (DuplexConnection _ rq _) -> subscribe rq
SomeConn _ (RcvConnection _ rq) -> subscribe rq
_ -> throwError $ CONN SIMPLEX
where
subscribe rq = subscribeQueue c rq cAlias >> respond' cAlias OK
resumeMsgDelivery :: forall m. AgentMonad m => AgentClient -> ConnId -> SndQueue -> m ()
resumeMsgDelivery c connId sq@SndQueue {server, sndId} = do
let qKey = (connId, server, sndId)
unlessM (queueDelivering qKey) $
async (runSmpQueueMsgDelivery c connId sq)
>>= atomically . modifyTVar (smpQueueMsgDeliveries c) . M.insert qKey
unlessM connQueued $
withStore (`getPendingMsgs` connId)
>>= queuePendingMsgs c connId sq
where
queueDelivering qKey = isJust . M.lookup qKey <$> readTVarIO (smpQueueMsgDeliveries c)
connQueued =
atomically $
isJust
<$> stateTVar
(connMsgsQueued c)
(\m -> (M.lookup connId m, M.insert connId True m))
-- TODO remove - hack for subscribing to all; respond' and parameterization of subscribeConnection are byproduct
subscribeAll :: m ()
subscribeAll = withStore (getAllConnAliases st) >>= mapM_ subscribeConnection
queuePendingMsgs :: AgentMonad m => AgentClient -> ConnId -> SndQueue -> [InternalId] -> m ()
queuePendingMsgs c connId sq msgIds = atomically $ do
q <- getPendingMsgQ c connId sq
mapM_ (writeTQueue q) msgIds
sendMessage :: MsgBody -> m ()
sendMessage msgBody =
withStore (getConn st connAlias) >>= \case
SomeConn _ (DuplexConnection _ _ sq) -> sendMsg sq
SomeConn _ (SndConnection _ sq) -> sendMsg sq
_ -> throwError $ CONN SIMPLEX
where
sendMsg sq = do
internalTs <- liftIO getCurrentTime
(internalId, internalSndId, previousMsgHash) <- withStore $ updateSndIds st sq
let msgStr =
serializeSMPMessage
SMPMessage
{ senderMsgId = unSndId internalSndId,
senderTimestamp = internalTs,
previousMsgHash,
agentMessage = A_MSG msgBody
}
msgHash = C.sha256Hash msgStr
withStore $
createSndMsg st sq $
SndMsgData {internalId, internalSndId, internalTs, msgBody, internalHash = msgHash}
sendAgentMessage c sq msgStr
respond $ SENT (unId internalId)
getPendingMsgQ :: AgentClient -> ConnId -> SndQueue -> STM (TQueue InternalId)
getPendingMsgQ c connId SndQueue {server, sndId} = do
let qKey = (connId, server, sndId)
maybe (newMsgQueue qKey) pure . M.lookup qKey =<< readTVar (smpQueueMsgQueues c)
where
newMsgQueue qKey = do
mq <- newTQueue
modifyTVar (smpQueueMsgQueues c) $ M.insert qKey mq
pure mq
suspendConnection :: m ()
suspendConnection =
withStore (getConn st connAlias) >>= \case
SomeConn _ (DuplexConnection _ rq _) -> suspend rq
SomeConn _ (RcvConnection _ rq) -> suspend rq
_ -> throwError $ CONN SIMPLEX
where
suspend rq = suspendQueue c rq >> respond OK
runSmpQueueMsgDelivery :: forall m. AgentMonad m => AgentClient -> ConnId -> SndQueue -> m ()
runSmpQueueMsgDelivery c@AgentClient {subQ} connId sq = do
mq <- atomically $ getPendingMsgQ c connId sq
ri <- asks $ reconnectInterval . config
forever $ do
msgId <- atomically $ readTQueue mq
let mId = unId msgId
withStore (\st -> E.try $ getPendingMsgData st connId msgId) >>= \case
Left (e :: E.SomeException) ->
notify $ MERR mId (INTERNAL $ show e)
Right (rq_, (msgType, msgBody, internalTs)) ->
withRetryInterval ri $ \loop ->
tryError (sendAgentMessage c sq msgBody) >>= \case
Left e -> do
case e of
SMP SMP.QUOTA -> loop
SMP SMP.AUTH -> case msgType of
HELLO_ -> do
helloTimeout <- asks $ helloTimeout . config
currentTime <- liftIO getCurrentTime
if diffUTCTime currentTime internalTs > helloTimeout
then case rq_ of
-- party initiating connection
Just _ -> notifyDel msgId . ERR $ CONN NOT_AVAILABLE
-- party joining connection
_ -> notifyDel msgId . ERR $ CONN NOT_ACCEPTED
else loop
REPLY_ -> notifyDel msgId $ ERR e
A_MSG_ -> notifyDel msgId $ MERR mId e
SMP (SMP.CMD _) -> notifyDel msgId $ MERR mId e
SMP SMP.LARGE_MSG -> notifyDel msgId $ MERR mId e
SMP {} -> notify (MERR mId e) >> loop
_ -> loop
Right () -> do
case msgType of
HELLO_ -> do
withStore $ \st -> setSndQueueStatus st sq Active
case rq_ of
-- party initiating connection
Just rq -> do
subscribeQueue c rq connId
notify CON
-- party joining connection
_ -> createReplyQueue c connId sq
A_MSG_ -> notify $ SENT mId
_ -> pure ()
delMsg msgId
where
delMsg :: InternalId -> m ()
delMsg msgId = withStore $ \st -> deleteMsg st connId msgId
notify :: ACommand 'Agent -> m ()
notify cmd = atomically $ writeTBQueue subQ ("", connId, cmd)
notifyDel :: InternalId -> ACommand 'Agent -> m ()
notifyDel msgId cmd = notify cmd >> delMsg msgId
deleteConnection :: m ()
deleteConnection =
withStore (getConn st connAlias) >>= \case
SomeConn _ (DuplexConnection _ rq _) -> delete rq
SomeConn _ (RcvConnection _ rq) -> delete rq
_ -> delConn
where
delConn = withStore (deleteConn st connAlias) >> respond OK
delete rq = do
deleteQueue c rq
removeSubscription c connAlias
delConn
ackMessage' :: forall m. AgentMonad m => AgentClient -> ConnId -> AgentMsgId -> m ()
ackMessage' c connId msgId = do
withStore (`getConn` connId) >>= \case
SomeConn _ (DuplexConnection _ rq _) -> ack rq
SomeConn _ (RcvConnection _ rq) -> ack rq
_ -> throwError $ CONN SIMPLEX
where
ack :: RcvQueue -> m ()
ack rq = do
let mId = InternalId msgId
withStore $ \st -> checkRcvMsg st connId mId
sendAck c rq
withStore $ \st -> deleteMsg st connId mId
createReplyQueue :: SndQueue -> m ()
createReplyQueue sq = do
srv <- getSMPServer
(rq, qInfo) <- newReceiveQueue c srv connAlias
withStore $ upgradeSndConnToDuplex st connAlias rq
senderTimestamp <- liftIO getCurrentTime
sendAgentMessage c sq . serializeSMPMessage $
SMPMessage
{ senderMsgId = 0,
senderTimestamp,
previousMsgHash = "",
agentMessage = REPLY qInfo
}
-- | Suspend SMP agent connection (OFF command) in Reader monad
suspendConnection' :: AgentMonad m => AgentClient -> ConnId -> m ()
suspendConnection' c connId =
withStore (`getConn` connId) >>= \case
SomeConn _ (DuplexConnection _ rq _) -> suspendQueue c rq
SomeConn _ (RcvConnection _ rq) -> suspendQueue c rq
_ -> throwError $ CONN SIMPLEX
respond :: ACommand 'Agent -> m ()
respond = respond' connAlias
-- | Delete SMP agent connection (DEL command) in Reader monad
deleteConnection' :: forall m. AgentMonad m => AgentClient -> ConnId -> m ()
deleteConnection' c connId =
withStore (`getConn` connId) >>= \case
SomeConn _ (DuplexConnection _ rq _) -> delete rq
SomeConn _ (RcvConnection _ rq) -> delete rq
SomeConn _ (ContactConnection _ rq) -> delete rq
SomeConn _ (SndConnection _ _) -> withStore (`deleteConn` connId)
where
delete :: RcvQueue -> m ()
delete rq = do
deleteQueue c rq
removeSubscription c connId
withStore (`deleteConn` connId)
respond' :: ConnAlias -> ACommand 'Agent -> m ()
respond' cAlias resp = atomically $ writeTBQueue sndQ (corrId, cAlias, resp)
getSMPServer :: AgentMonad m => m SMPServer
getSMPServer =
asks (smpServers . config) >>= \case
srv :| [] -> pure srv
servers -> do
gen <- asks randomServer
i <- atomically . stateTVar gen $ randomR (0, L.length servers - 1)
pure $ servers L.!! i
subscriber :: (MonadUnliftIO m, MonadReader Env m) => AgentClient -> SQLiteStore -> m ()
subscriber c@AgentClient {msgQ} st = forever $ do
-- TODO this will only process messages and notifications
subscriber :: (MonadUnliftIO m, MonadReader Env m) => AgentClient -> m ()
subscriber c@AgentClient {msgQ} = forever $ do
t <- atomically $ readTBQueue msgQ
runExceptT (processSMPTransmission c st t) >>= \case
withAgentLock c (runExceptT $ processSMPTransmission c t) >>= \case
Left e -> liftIO $ print e
Right _ -> return ()
processSMPTransmission :: forall m. AgentMonad m => AgentClient -> SQLiteStore -> SMPServerTransmission -> m ()
processSMPTransmission c@AgentClient {sndQ} st (srv, rId, cmd) = do
withStore (getRcvConn st srv rId) >>= \case
SomeConn SCDuplex (DuplexConnection _ rq _) -> processSMP SCDuplex rq
SomeConn SCRcv (RcvConnection _ rq) -> processSMP SCRcv rq
_ -> atomically $ writeTBQueue sndQ ("", "", ERR $ CONN SIMPLEX)
processSMPTransmission :: forall m. AgentMonad m => AgentClient -> SMPServerTransmission -> m ()
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 -> RcvQueue -> m ()
processSMP cType rq@RcvQueue {connAlias, status} =
processSMP :: SConnType c -> ConnData -> RcvQueue -> m ()
processSMP cType ConnData {connId} rq@RcvQueue {rcvDhSecret, e2ePrivKey, e2eDhSecret, status} =
case cmd of
SMP.MSG srvMsgId srvTs msgBody -> do
SMP.MSG srvMsgId srvTs msgBody' -> handleNotifyAck $ do
-- TODO deduplicate with previously received
msg <- decryptAndVerify rq msgBody
let msgHash = C.sha256Hash msg
agentMsg <- liftEither $ parseSMPMessage msg
case agentMsg of
SMPConfirmation senderKey -> smpConfirmation senderKey
SMPMessage {agentMessage, senderMsgId, senderTimestamp, previousMsgHash} ->
case agentMessage of
HELLO verifyKey _ -> helloMsg verifyKey msgBody
REPLY qInfo -> replyMsg qInfo
A_MSG body -> agentClientMsg previousMsgHash (senderMsgId, senderTimestamp) (srvMsgId, srvTs) body msgHash
sendAck c rq
return ()
msgBody <- agentCbDecrypt rcvDhSecret (C.cbNonce srvMsgId) msgBody'
clientMsg@SMP.ClientMsgEnvelope {cmHeader = SMP.PubHeader phVer e2ePubKey_} <-
parseMessage msgBody
unless (phVer `isCompatible` SMP.smpClientVRange) . throwError $ AGENT A_VERSION
case (e2eDhSecret, e2ePubKey_) of
(Nothing, Just e2ePubKey) -> do
let e2eDh = C.dh' e2ePubKey e2ePrivKey
decryptClientMessage e2eDh clientMsg >>= \case
(SMP.PHConfirmation senderKey, AgentConfirmation {e2eEncryption, encConnInfo}) ->
smpConfirmation senderKey e2ePubKey e2eEncryption encConnInfo >> ack
(SMP.PHEmpty, AgentInvitation {connReq, connInfo}) ->
smpInvitation connReq connInfo >> ack
_ -> prohibited >> ack
(Just e2eDh, Nothing) -> do
decryptClientMessage e2eDh clientMsg >>= \case
(SMP.PHEmpty, AgentMsgEnvelope _ encAgentMsg) -> do
agentMsgBody <- agentRatchetDecrypt connId encAgentMsg
parseMessage agentMsgBody >>= \case
AgentMessage APrivHeader {sndMsgId, prevMsgHash} aMessage -> do
(msgId, msgMeta) <- agentClientMsg prevMsgHash sndMsgId (srvMsgId, systemToUTCTime srvTs) agentMsgBody aMessage
case aMessage of
HELLO -> helloMsg >> ack >> withStore (\st -> deleteMsg st connId msgId)
REPLY cReq -> replyMsg cReq >> ack >> withStore (\st -> deleteMsg st connId msgId)
-- note that there is no ACK sent here, it is sent with agent's user ACK command
A_MSG body -> notify $ MSG msgMeta body
_ -> prohibited >> ack
_ -> prohibited >> ack
_ -> prohibited >> ack
SMP.END -> do
removeSubscription c connAlias
removeSubscription c connId
logServer "<--" c srv rId "END"
notify END
_ -> do
@@ -311,76 +538,108 @@ processSMPTransmission c@AgentClient {sndQ} st (srv, rId, cmd) = do
notify . ERR $ BROKER UNEXPECTED
where
notify :: ACommand 'Agent -> m ()
notify msg = atomically $ writeTBQueue sndQ ("", connAlias, msg)
notify msg = atomically $ writeTBQueue subQ ("", connId, msg)
handleNotifyAck :: m () -> m ()
handleNotifyAck m = m `catchError` \e -> notify (ERR e) >> ack
prohibited :: m ()
prohibited = notify . ERR $ AGENT A_PROHIBITED
smpConfirmation :: SenderPublicKey -> m ()
smpConfirmation senderKey = do
logServer "<--" c srv rId "MSG <KEY>"
ack :: m ()
ack = sendAck c rq
decryptClientMessage :: C.DhSecretX25519 -> SMP.ClientMsgEnvelope -> m (SMP.PrivHeader, AgentMsgEnvelope)
decryptClientMessage e2eDh SMP.ClientMsgEnvelope {cmNonce, cmEncBody} = do
clientMsg <- agentCbDecrypt e2eDh cmNonce cmEncBody
SMP.ClientMessage privHeader clientBody <- parseMessage clientMsg
agentEnvelope <- parseMessage clientBody
if agentVersion agentEnvelope `isCompatible` smpAgentVRange
then pure (privHeader, agentEnvelope)
else throwError $ AGENT A_VERSION
parseMessage :: Encoding a => ByteString -> m a
parseMessage = liftEither . parse smpP (AGENT A_MESSAGE)
smpConfirmation :: C.APublicVerifyKey -> C.PublicKeyX25519 -> Maybe (CR.E2ERatchetParams 'C.X448) -> ByteString -> m ()
smpConfirmation senderKey e2ePubKey e2eEncryption encConnInfo = do
logServer "<--" c srv rId "MSG <CONF>"
case status of
New -> do
-- TODO currently it automatically allows whoever sends the confirmation
-- Commands CONF and LET are not supported in v0.2
withStore $ setRcvQueueStatus st rq Confirmed
-- TODO update sender key in the store?
secureQueue c rq senderKey
withStore $ setRcvQueueStatus st rq Secured
New -> case (cType, e2eEncryption) of
(SCRcv, Just e2eSndParams) -> do
(pk1, rcDHRs) <- withStore $ \st -> getRatchetX3dhKeys st connId
let rc = CR.initRcvRatchet rcDHRs $ CR.x3dhRcv pk1 rcDHRs e2eSndParams
(agentMsgBody_, rc', skipped) <- liftError cryptoError $ CR.rcDecrypt rc M.empty encConnInfo
case (agentMsgBody_, skipped) of
(Right agentMsgBody, CR.SMDNoChange) ->
parseMessage agentMsgBody >>= \case
AgentConnInfo connInfo -> do
g <- asks idsDrg
let senderConf = SMPConfirmation {senderKey, e2ePubKey, connInfo}
newConfirmation = NewConfirmation {connId, senderConf, ratchetState = rc'}
confId <- withStore $ \st -> createConfirmation st g newConfirmation
notify $ CONF confId connInfo
_ -> prohibited
_ -> prohibited
(SCDuplex, Nothing) -> do
agentRatchetDecrypt connId encConnInfo >>= parseMessage >>= \case
AgentConnInfo connInfo -> do
notify $ INFO connInfo
processConfirmation c rq $ SMPConfirmation {senderKey, e2ePubKey, connInfo}
_ -> prohibited
_ -> prohibited
_ -> prohibited
helloMsg :: SenderPublicKey -> ByteString -> m ()
helloMsg verifyKey msgBody = do
helloMsg :: m ()
helloMsg = do
logServer "<--" c srv rId "MSG <HELLO>"
case status of
Active -> prohibited
_ -> do
void $ verifyMessage (Just verifyKey) msgBody
withStore $ setRcvQueueActive st rq verifyKey
withStore $ \st -> setRcvQueueStatus st rq Active
case cType of
SCDuplex -> notify CON
SCDuplex -> notifyConnected c connId
_ -> pure ()
replyMsg :: SMPQueueInfo -> m ()
replyMsg qInfo = do
replyMsg :: L.NonEmpty SMPQueueInfo -> m ()
replyMsg (qInfo :| _) = do
logServer "<--" c srv rId "MSG <REPLY>"
case cType of
SCRcv -> do
(sq, senderKey, verifyKey) <- newSendQueue qInfo connAlias
withStore $ upgradeRcvConnToDuplex st connAlias sq
connectToSendQueue c st sq senderKey verifyKey
notify CON
AcceptedConfirmation {ownConnInfo} <- withStore (`getAcceptedConfirmation` connId)
case qInfo `proveCompatible` SMP.smpClientVRange of
Nothing -> notify . ERR $ AGENT A_VERSION
Just qInfo' -> do
(sq, smpConf) <- newSndQueue qInfo' ownConnInfo
withStore $ \st -> upgradeRcvConnToDuplex st connId sq
confirmQueue c connId sq smpConf Nothing
withStore (`removeConfirmations` connId)
void $ enqueueMessage c connId sq HELLO
_ -> prohibited
agentClientMsg :: PrevRcvMsgHash -> (ExternalSndId, ExternalSndTs) -> (BrokerId, BrokerTs) -> MsgBody -> MsgHash -> m ()
agentClientMsg receivedPrevMsgHash senderMeta brokerMeta msgBody msgHash = do
agentClientMsg :: PrevRcvMsgHash -> ExternalSndId -> (BrokerId, BrokerTs) -> MsgBody -> AMessage -> m (InternalId, MsgMeta)
agentClientMsg externalPrevSndHash sndMsgId broker msgBody aMessage = do
logServer "<--" c srv rId "MSG <MSG>"
case status of
Active -> do
internalTs <- liftIO getCurrentTime
(internalId, internalRcvId, prevExtSndId, prevRcvMsgHash) <- withStore $ updateRcvIds st rq
let msgIntegrity = checkMsgIntegrity prevExtSndId (fst senderMeta) prevRcvMsgHash receivedPrevMsgHash
withStore $
createRcvMsg st rq $
RcvMsgData
{ internalId,
internalRcvId,
internalTs,
senderMeta,
brokerMeta,
msgBody,
internalHash = msgHash,
externalPrevSndHash = receivedPrevMsgHash,
msgIntegrity
}
notify
MSG
{ recipientMeta = (unId internalId, internalTs),
senderMeta,
brokerMeta,
msgBody,
msgIntegrity
}
let internalHash = C.sha256Hash msgBody
internalTs <- liftIO getCurrentTime
(internalId, internalRcvId, prevExtSndId, prevRcvMsgHash) <- withStore (`updateRcvIds` connId)
let integrity = checkMsgIntegrity prevExtSndId sndMsgId prevRcvMsgHash externalPrevSndHash
recipient = (unId internalId, internalTs)
msgMeta = MsgMeta {integrity, recipient, broker, sndMsgId}
msgType = aMessageType aMessage
rcvMsg = RcvMsgData {msgMeta, msgType, msgBody, internalRcvId, internalHash, externalPrevSndHash}
withStore $ \st -> createRcvMsg st connId rcvMsg
pure (internalId, msgMeta)
smpInvitation :: ConnectionRequestUri 'CMInvitation -> ConnInfo -> m ()
smpInvitation connReq cInfo = do
logServer "<--" c srv rId "MSG <KEY>"
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 invId cInfo
_ -> prohibited
checkMsgIntegrity :: PrevExternalSndId -> ExternalSndId -> PrevRcvMsgHash -> ByteString -> MsgIntegrity
@@ -392,27 +651,60 @@ processSMPTransmission c@AgentClient {sndQ} st (srv, rId, cmd) = do
| internalPrevMsgHash /= receivedPrevMsgHash = MsgError MsgBadHash
| otherwise = MsgError MsgDuplicate -- this case is not possible
connectToSendQueue :: AgentMonad m => AgentClient -> SQLiteStore -> SndQueue -> SenderPublicKey -> VerificationKey -> m ()
connectToSendQueue c st sq senderKey verifyKey = do
sendConfirmation c sq senderKey
withStore $ setSndQueueStatus st sq Confirmed
sendHello c sq verifyKey
withStore $ setSndQueueStatus st sq Active
confirmQueue :: forall m. AgentMonad m => AgentClient -> ConnId -> SndQueue -> SMPConfirmation -> Maybe (CR.E2ERatchetParams 'C.X448) -> m ()
confirmQueue c connId sq SMPConfirmation {senderKey, e2ePubKey, connInfo} e2eEncryption = do
msg <- mkConfirmation
sendConfirmation c sq msg
withStore $ \st -> setSndQueueStatus st sq Confirmed
where
mkConfirmation :: m MsgBody
mkConfirmation = do
encConnInfo <- agentRatchetEncrypt connId (smpEncode $ AgentConnInfo connInfo) e2eEncConnInfoLength
let agentEnvelope = AgentConfirmation {agentVersion = smpAgentVersion, e2eEncryption, encConnInfo}
agentCbEncrypt sq (Just e2ePubKey) . smpEncode $
SMP.ClientMessage (SMP.PHConfirmation senderKey) $ smpEncode agentEnvelope
newSendQueue ::
(MonadUnliftIO m, MonadReader Env m) => SMPQueueInfo -> ConnAlias -> m (SndQueue, SenderPublicKey, VerificationKey)
newSendQueue (SMPQueueInfo smpServer senderId encryptKey) connAlias = do
size <- asks $ rsaKeySize . config
(senderKey, sndPrivateKey) <- liftIO $ C.generateKeyPair size
(verifyKey, signKey) <- liftIO $ C.generateKeyPair size
-- encoded AgentMessage -> encoded EncAgentMessage
agentRatchetEncrypt :: AgentMonad m => ConnId -> ByteString -> Int -> m ByteString
agentRatchetEncrypt connId msg paddedLen = do
rc <- withStore $ \st -> getRatchet st connId
(encMsg, rc') <- liftError cryptoError $ CR.rcEncrypt rc paddedLen msg
withStore $ \st -> updateRatchet st connId rc' CR.SMDNoChange
pure encMsg
-- encoded EncAgentMessage -> encoded AgentMessage
agentRatchetDecrypt :: AgentMonad m => ConnId -> ByteString -> m ByteString
agentRatchetDecrypt connId encAgentMsg = do
(rc, skipped) <- withStore $ \st ->
(,) <$> getRatchet st connId <*> getSkippedMsgKeys st connId
(agentMsgBody_, rc', skippedDiff) <- liftError cryptoError $ CR.rcDecrypt rc skipped encAgentMsg
withStore $ \st -> updateRatchet st connId rc' skippedDiff
liftEither $ first cryptoError agentMsgBody_
notifyConnected :: AgentMonad m => AgentClient -> ConnId -> m ()
notifyConnected c connId = atomically $ writeTBQueue (subQ c) ("", connId, CON)
newSndQueue :: (MonadUnliftIO m, MonadReader Env m) => Compatible SMPQueueInfo -> ConnInfo -> m (SndQueue, SMPConfirmation)
newSndQueue qInfo cInfo =
asks (cmdSignAlg . config) >>= \case
C.SignAlg a -> newSndQueue_ a qInfo cInfo
newSndQueue_ ::
(C.SignatureAlgorithm a, C.AlgorithmI a, MonadUnliftIO m) =>
C.SAlgorithm a ->
Compatible SMPQueueInfo ->
ConnInfo ->
m (SndQueue, SMPConfirmation)
newSndQueue_ a (Compatible (SMPQueueInfo _clientVersion smpServer senderId rcvE2ePubDhKey)) cInfo = do
-- this function assumes clientVersion is compatible - it was tested before
(senderKey, sndPrivateKey) <- liftIO $ C.generateSignatureKeyPair a
(e2ePubKey, e2ePrivKey) <- liftIO C.generateKeyPair'
let sndQueue =
SndQueue
{ server = smpServer,
sndId = senderId,
connAlias,
sndPrivateKey,
encryptKey,
signKey,
e2eDhSecret = C.dh' rcvE2ePubDhKey e2ePrivKey,
status = New
}
return (sndQueue, senderKey, verifyKey)
pure (sndQueue, SMPConfirmation senderKey e2ePubKey cInfo)
+297 -142
View File
@@ -2,126 +2,264 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Simplex.Messaging.Agent.Client
( AgentClient (..),
newAgentClient,
AgentMonad,
getSMPServerClient,
closeSMPServerClients,
newReceiveQueue,
withAgentLock,
closeAgentClient,
newRcvQueue,
subscribeQueue,
addSubscription,
sendConfirmation,
sendHello,
sendInvitation,
RetryInterval (..),
secureQueue,
sendAgentMessage,
decryptAndVerify,
verifyMessage,
agentCbEncrypt,
agentCbDecrypt,
cryptoError,
sendAck,
suspendQueue,
deleteQueue,
logServer,
removeSubscription,
cryptoError,
)
where
import Control.Concurrent (forkIO)
import Control.Concurrent.Async (Async, uninterruptibleCancel)
import Control.Concurrent.STM (stateTVar)
import Control.Logger.Simple
import Control.Monad.Except
import Control.Monad.IO.Unlift
import Control.Monad.Reader
import Control.Monad.Trans.Except
import Data.Bifunctor (first)
import Data.ByteString.Base64
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Data.Maybe (isNothing)
import Data.Set (Set)
import qualified Data.Set as S
import Data.Text.Encoding
import Data.Time.Clock
import Simplex.Messaging.Agent.Env.SQLite
import Simplex.Messaging.Agent.Protocol
import Simplex.Messaging.Agent.RetryInterval
import Simplex.Messaging.Agent.Store
import Simplex.Messaging.Client
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Protocol (ErrorType (AUTH), MsgBody, QueueId, SenderPublicKey)
import Simplex.Messaging.Util (bshow, liftEitherError, liftError)
import UnliftIO.Concurrent
import UnliftIO.Exception (IOException)
import Simplex.Messaging.Encoding
import Simplex.Messaging.Protocol (QueueId, QueueIdsKeys (..), SndPublicVerifyKey)
import qualified Simplex.Messaging.Protocol as SMP
import Simplex.Messaging.Util (bshow, liftEitherError, liftError, tryError)
import Simplex.Messaging.Version
import System.Timeout (timeout)
import UnliftIO (async)
import UnliftIO.Exception (Exception, IOException)
import qualified UnliftIO.Exception as E
import UnliftIO.STM
type SMPClientVar = TMVar (Either AgentErrorType SMPClient)
data AgentClient = AgentClient
{ rcvQ :: TBQueue (ATransmission 'Client),
sndQ :: TBQueue (ATransmission 'Agent),
subQ :: TBQueue (ATransmission 'Agent),
msgQ :: TBQueue SMPServerTransmission,
smpClients :: TVar (Map SMPServer SMPClient),
subscrSrvrs :: TVar (Map SMPServer (Set ConnAlias)),
subscrConns :: TVar (Map ConnAlias SMPServer),
clientId :: Int
smpClients :: TVar (Map SMPServer SMPClientVar),
subscrSrvrs :: TVar (Map SMPServer (Map ConnId RcvQueue)),
pendingSubscrSrvrs :: TVar (Map SMPServer (Map ConnId RcvQueue)),
subscrConns :: TVar (Map ConnId SMPServer),
connMsgsQueued :: TVar (Map ConnId Bool),
smpQueueMsgQueues :: TVar (Map (ConnId, SMPServer, SMP.SenderId) (TQueue InternalId)),
smpQueueMsgDeliveries :: TVar (Map (ConnId, SMPServer, SMP.SenderId) (Async ())),
reconnections :: TVar [Async ()],
asyncClients :: TVar [Async ()],
clientId :: Int,
agentEnv :: Env,
smpSubscriber :: Async (),
lock :: TMVar ()
}
newAgentClient :: TVar Int -> AgentConfig -> STM AgentClient
newAgentClient cc AgentConfig {tbqSize} = do
rcvQ <- newTBQueue tbqSize
sndQ <- newTBQueue tbqSize
msgQ <- newTBQueue tbqSize
newAgentClient :: Env -> STM AgentClient
newAgentClient agentEnv = do
let qSize = tbqSize $ config agentEnv
rcvQ <- newTBQueue qSize
subQ <- newTBQueue qSize
msgQ <- newTBQueue qSize
smpClients <- newTVar M.empty
subscrSrvrs <- newTVar M.empty
pendingSubscrSrvrs <- newTVar M.empty
subscrConns <- newTVar M.empty
clientId <- (+ 1) <$> readTVar cc
writeTVar cc clientId
return AgentClient {rcvQ, sndQ, msgQ, smpClients, subscrSrvrs, subscrConns, clientId}
connMsgsQueued <- newTVar M.empty
smpQueueMsgQueues <- newTVar M.empty
smpQueueMsgDeliveries <- newTVar M.empty
reconnections <- newTVar []
asyncClients <- newTVar []
clientId <- stateTVar (clientCounter agentEnv) $ \i -> (i + 1, i + 1)
lock <- newTMVar ()
return AgentClient {rcvQ, subQ, msgQ, smpClients, subscrSrvrs, pendingSubscrSrvrs, subscrConns, connMsgsQueued, smpQueueMsgQueues, smpQueueMsgDeliveries, reconnections, asyncClients, clientId, agentEnv, smpSubscriber = undefined, lock}
-- | Agent monad with MonadReader Env and MonadError AgentErrorType
type AgentMonad m = (MonadUnliftIO m, MonadReader Env m, MonadError AgentErrorType m)
newtype InternalException e = InternalException {unInternalException :: e}
deriving (Eq, Show)
instance Exception e => Exception (InternalException e)
instance (MonadUnliftIO m, Exception e) => MonadUnliftIO (ExceptT e m) where
withRunInIO :: ((forall a. ExceptT e m a -> IO a) -> IO b) -> ExceptT e m b
withRunInIO exceptToIO =
withExceptT unInternalException . ExceptT . E.try $
withRunInIO $ \run ->
exceptToIO $ run . (either (E.throwIO . InternalException) return <=< runExceptT)
getSMPServerClient :: forall m. AgentMonad m => AgentClient -> SMPServer -> m SMPClient
getSMPServerClient c@AgentClient {smpClients, msgQ} srv =
readTVarIO smpClients
>>= maybe newSMPClient return . M.lookup srv
atomically getClientVar >>= either newSMPClient waitForSMPClient
where
newSMPClient :: m SMPClient
newSMPClient = do
smp <- connectClient
logInfo . decodeUtf8 $ "Agent connected to " <> showServer srv
atomically . modifyTVar smpClients $ M.insert srv smp
return smp
getClientVar :: STM (Either SMPClientVar SMPClientVar)
getClientVar = maybe (Left <$> newClientVar) (pure . Right) . M.lookup srv =<< readTVar smpClients
newClientVar :: STM SMPClientVar
newClientVar = do
smpVar <- newEmptyTMVar
modifyTVar smpClients $ M.insert srv smpVar
pure smpVar
waitForSMPClient :: TMVar (Either AgentErrorType SMPClient) -> m SMPClient
waitForSMPClient smpVar = do
SMPClientConfig {tcpTimeout} <- asks $ smpCfg . config
smpClient_ <- liftIO $ tcpTimeout `timeout` atomically (readTMVar smpVar)
liftEither $ case smpClient_ of
Just (Right smpClient) -> Right smpClient
Just (Left e) -> Left e
Nothing -> Left $ BROKER TIMEOUT
newSMPClient :: TMVar (Either AgentErrorType SMPClient) -> m SMPClient
newSMPClient smpVar = tryConnectClient pure tryConnectAsync
where
tryConnectClient :: (SMPClient -> m a) -> m () -> m a
tryConnectClient successAction retryAction =
tryError connectClient >>= \r -> case r of
Right smp -> do
logInfo . decodeUtf8 $ "Agent connected to " <> showServer srv
atomically $ putTMVar smpVar r
successAction smp
Left e -> do
if e == BROKER NETWORK || e == BROKER TIMEOUT
then retryAction
else atomically $ do
putTMVar smpVar (Left e)
modifyTVar smpClients $ M.delete srv
throwError e
tryConnectAsync :: m ()
tryConnectAsync = do
a <- async connectAsync
atomically $ modifyTVar (asyncClients c) (a :)
connectAsync :: m ()
connectAsync = do
ri <- asks $ reconnectInterval . config
withRetryInterval ri $ \loop -> void $ tryConnectClient (const reconnectClient) loop
connectClient :: m SMPClient
connectClient = do
cfg <- asks $ smpCfg . config
liftEitherError smpClientError (getSMPClient srv cfg msgQ clientDisconnected)
u <- askUnliftIO
liftEitherError smpClientError (getSMPClient srv cfg msgQ $ clientDisconnected u)
`E.catch` internalError
where
internalError :: IOException -> m SMPClient
internalError = throwError . INTERNAL . show
clientDisconnected :: IO ()
clientDisconnected = do
removeSubs >>= mapM_ (mapM_ notifySub)
clientDisconnected :: UnliftIO m -> IO ()
clientDisconnected u = do
removeClientSubs >>= (`forM_` serverDown u)
logInfo . decodeUtf8 $ "Agent disconnected from " <> showServer srv
removeSubs :: IO (Maybe (Set ConnAlias))
removeSubs = atomically $ do
removeClientSubs :: IO (Maybe (Map ConnId RcvQueue))
removeClientSubs = atomically $ do
modifyTVar smpClients $ M.delete srv
cs <- M.lookup srv <$> readTVar (subscrSrvrs c)
modifyTVar (subscrSrvrs c) $ M.delete srv
modifyTVar (subscrConns c) $ maybe id deleteKeys cs
modifyTVar (subscrConns c) $ maybe id (deleteKeys . M.keysSet) cs
mapM_ (modifyTVar (pendingSubscrSrvrs c) . addPendingSubs) cs
return cs
where
addPendingSubs :: Map ConnId RcvQueue -> Map SMPServer (Map ConnId RcvQueue) -> Map SMPServer (Map ConnId RcvQueue)
addPendingSubs cs = M.alter (Just . addSubs cs) srv
addSubs cs = maybe cs (M.union cs)
deleteKeys :: Ord k => Set k -> Map k a -> Map k a
deleteKeys ks m = S.foldr' M.delete m ks
notifySub :: ConnAlias -> IO ()
notifySub connAlias = atomically $ writeTBQueue (sndQ c) ("", connAlias, END)
serverDown :: UnliftIO m -> Map ConnId RcvQueue -> IO ()
serverDown u cs = unless (M.null cs) $ do
mapM_ (notifySub DOWN) $ M.keysSet cs
unliftIO u reconnectServer
closeSMPServerClients :: MonadUnliftIO m => AgentClient -> m ()
closeSMPServerClients c = liftIO $ readTVarIO (smpClients c) >>= mapM_ closeSMPClient
reconnectServer :: m ()
reconnectServer = do
a <- async tryReconnectClient
atomically $ modifyTVar (reconnections c) (a :)
tryReconnectClient :: m ()
tryReconnectClient = do
ri <- asks $ reconnectInterval . config
withRetryInterval ri $ \loop ->
reconnectClient `catchError` const loop
reconnectClient :: m ()
reconnectClient = do
withAgentLock c . withSMP c srv $ \smp -> do
subs <- readTVarIO $ subscrConns c
cs <- M.lookup srv <$> readTVarIO (pendingSubscrSrvrs c)
forM_ (maybe [] M.toList cs) $ \(connId, rq@RcvQueue {rcvPrivateKey, rcvId}) ->
when (isNothing $ M.lookup connId subs) $ do
subscribeSMPQueue smp rcvPrivateKey rcvId
`catchError` \case
e@SMPResponseTimeout -> throwError e
e@SMPNetworkError -> throwError e
e -> liftIO $ notifySub (ERR $ smpClientError e) connId
addSubscription c rq connId
liftIO $ notifySub UP connId
notifySub :: ACommand 'Agent -> ConnId -> IO ()
notifySub cmd connId = atomically $ writeTBQueue (subQ c) ("", connId, cmd)
closeAgentClient :: MonadUnliftIO m => AgentClient -> m ()
closeAgentClient c = liftIO $ do
closeSMPServerClients c
cancelActions $ reconnections c
cancelActions $ asyncClients c
cancelActions $ smpQueueMsgDeliveries c
closeSMPServerClients :: AgentClient -> IO ()
closeSMPServerClients c = readTVarIO (smpClients c) >>= mapM_ (forkIO . closeClient)
where
closeClient smpVar =
atomically (readTMVar smpVar) >>= \case
Right smp -> closeSMPClient smp `E.catch` \(_ :: E.SomeException) -> pure ()
_ -> pure ()
cancelActions :: Foldable f => TVar (f (Async ())) -> IO ()
cancelActions as = readTVarIO as >>= mapM_ uninterruptibleCancel
withAgentLock :: MonadUnliftIO m => AgentClient -> m a -> m a
withAgentLock AgentClient {lock} =
E.bracket_
(void . atomically $ takeTMVar lock)
(atomically $ putTMVar lock ())
withSMP_ :: forall a m. AgentMonad m => AgentClient -> SMPServer -> (SMPClient -> m a) -> m a
withSMP_ c srv action =
@@ -158,103 +296,111 @@ smpClientError = \case
SMPTransportError e -> BROKER $ TRANSPORT e
e -> INTERNAL $ show e
newReceiveQueue :: AgentMonad m => AgentClient -> SMPServer -> ConnAlias -> m (RcvQueue, SMPQueueInfo)
newReceiveQueue c srv connAlias = do
size <- asks $ rsaKeySize . config
(recipientKey, rcvPrivateKey) <- liftIO $ C.generateKeyPair size
newRcvQueue :: AgentMonad m => AgentClient -> SMPServer -> m (RcvQueue, SMPQueueUri)
newRcvQueue c srv =
asks (cmdSignAlg . config) >>= \case
C.SignAlg a -> newRcvQueue_ a c srv
newRcvQueue_ ::
(C.SignatureAlgorithm a, C.AlgorithmI a, AgentMonad m) =>
C.SAlgorithm a ->
AgentClient ->
SMPServer ->
m (RcvQueue, SMPQueueUri)
newRcvQueue_ a c srv = do
(recipientKey, rcvPrivateKey) <- liftIO $ C.generateSignatureKeyPair a
(dhKey, privDhKey) <- liftIO C.generateKeyPair'
(e2eDhKey, e2ePrivKey) <- liftIO C.generateKeyPair'
logServer "-->" c srv "" "NEW"
(rcvId, sId) <- withSMP c srv $ \smp -> createSMPQueue smp rcvPrivateKey recipientKey
logServer "<--" c srv "" $ B.unwords ["IDS", logSecret rcvId, logSecret sId]
(encryptKey, decryptKey) <- liftIO $ C.generateKeyPair size
QIK {rcvId, sndId, rcvPublicDhKey} <-
withSMP c srv $ \smp -> createSMPQueue smp rcvPrivateKey recipientKey dhKey
logServer "<--" c srv "" $ B.unwords ["IDS", logSecret rcvId, logSecret sndId]
let rq =
RcvQueue
{ server = srv,
rcvId,
connAlias,
rcvPrivateKey,
sndId = Just sId,
sndKey = Nothing,
decryptKey,
verifyKey = Nothing,
rcvDhSecret = C.dh' rcvPublicDhKey privDhKey,
e2ePrivKey,
e2eDhSecret = Nothing,
sndId = Just sndId,
status = New
}
addSubscription c rq connAlias
return (rq, SMPQueueInfo srv sId encryptKey)
pure (rq, SMPQueueUri srv sndId SMP.smpClientVRange e2eDhKey)
subscribeQueue :: AgentMonad m => AgentClient -> RcvQueue -> ConnAlias -> m ()
subscribeQueue c rq@RcvQueue {server, rcvPrivateKey, rcvId} connAlias = do
withLogSMP c server rcvId "SUB" $ \smp ->
subscribeSMPQueue smp rcvPrivateKey rcvId
addSubscription c rq connAlias
subscribeQueue :: AgentMonad m => AgentClient -> RcvQueue -> ConnId -> m ()
subscribeQueue c rq@RcvQueue {server, rcvPrivateKey, rcvId} connId = do
addPendingSubscription c rq connId
withLogSMP c server rcvId "SUB" $ \smp -> do
liftIO (runExceptT $ subscribeSMPQueue smp rcvPrivateKey rcvId) >>= \case
Left e -> do
atomically . when (e /= SMPNetworkError && e /= SMPResponseTimeout) $
removePendingSubscription c server connId
throwError e
Right _ -> addSubscription c rq connId
addSubscription :: MonadUnliftIO m => AgentClient -> RcvQueue -> ConnAlias -> m ()
addSubscription c RcvQueue {server} connAlias = atomically $ do
modifyTVar (subscrConns c) $ M.insert connAlias server
modifyTVar (subscrSrvrs c) $ M.alter (Just . addSub) server
addSubscription :: MonadUnliftIO m => AgentClient -> RcvQueue -> ConnId -> m ()
addSubscription c rq@RcvQueue {server} connId = atomically $ do
modifyTVar (subscrConns c) $ M.insert connId server
addSubs_ (subscrSrvrs c) rq connId
removePendingSubscription c server connId
addPendingSubscription :: MonadUnliftIO m => AgentClient -> RcvQueue -> ConnId -> m ()
addPendingSubscription c rq connId =
atomically $ addSubs_ (pendingSubscrSrvrs c) rq connId
addSubs_ :: TVar (Map SMPServer (Map ConnId RcvQueue)) -> RcvQueue -> ConnId -> STM ()
addSubs_ ss rq@RcvQueue {server} connId = modifyTVar ss $ M.alter (Just . addSub) server
where
addSub :: Maybe (Set ConnAlias) -> Set ConnAlias
addSub (Just cs) = S.insert connAlias cs
addSub _ = S.singleton connAlias
addSub = maybe (M.singleton connId rq) (M.insert connId rq)
removeSubscription :: AgentMonad m => AgentClient -> ConnAlias -> m ()
removeSubscription AgentClient {subscrConns, subscrSrvrs} connAlias = atomically $ do
cs <- readTVar subscrConns
writeTVar subscrConns $ M.delete connAlias cs
mapM_
(modifyTVar subscrSrvrs . M.alter (>>= delSub))
(M.lookup connAlias cs)
removeSubscription :: MonadUnliftIO m => AgentClient -> ConnId -> m ()
removeSubscription c@AgentClient {subscrConns} connId = atomically $ do
server_ <- stateTVar subscrConns $ \cs -> (M.lookup connId cs, M.delete connId cs)
mapM_ (\server -> removeSubs_ (subscrSrvrs c) server connId) server_
removePendingSubscription :: AgentClient -> SMPServer -> ConnId -> STM ()
removePendingSubscription c = removeSubs_ (pendingSubscrSrvrs c)
removeSubs_ :: TVar (Map SMPServer (Map ConnId RcvQueue)) -> SMPServer -> ConnId -> STM ()
removeSubs_ ss server connId = modifyTVar ss $ M.update delSub server
where
delSub :: Set ConnAlias -> Maybe (Set ConnAlias)
delSub :: Map ConnId RcvQueue -> Maybe (Map ConnId RcvQueue)
delSub cs =
let cs' = S.delete connAlias cs
in if S.null cs' then Nothing else Just cs'
let cs' = M.delete connId cs
in if M.null cs' then Nothing else Just cs'
logServer :: AgentMonad m => ByteString -> AgentClient -> SMPServer -> QueueId -> ByteString -> m ()
logServer dir AgentClient {clientId} srv qId cmdStr =
logInfo . decodeUtf8 $ B.unwords ["A", "(" <> bshow clientId <> ")", dir, showServer srv, ":", logSecret qId, cmdStr]
showServer :: SMPServer -> ByteString
showServer srv = B.pack $ host srv <> maybe "" (":" <>) (port srv)
showServer SMPServer {host, port} =
B.pack $ host <> if null port then "" else ':' : port
logSecret :: ByteString -> ByteString
logSecret bs = encode $ B.take 3 bs
sendConfirmation :: forall m. AgentMonad m => AgentClient -> SndQueue -> SenderPublicKey -> m ()
sendConfirmation c sq@SndQueue {server, sndId} senderKey =
withLogSMP_ c server sndId "SEND <KEY>" $ \smp -> do
msg <- mkConfirmation smp
liftSMP $ sendSMPMessage smp Nothing sndId msg
-- TODO maybe package E2ERatchetParams into SMPConfirmation
sendConfirmation :: forall m. AgentMonad m => AgentClient -> SndQueue -> ByteString -> m ()
sendConfirmation c SndQueue {server, sndId} encConfirmation =
withLogSMP_ c server sndId "SEND <CONF>" $ \smp ->
liftSMP $ sendSMPMessage smp Nothing sndId encConfirmation
sendInvitation :: forall m. AgentMonad m => AgentClient -> Compatible SMPQueueInfo -> ConnectionRequestUri 'CMInvitation -> ConnInfo -> m ()
sendInvitation c (Compatible SMPQueueInfo {smpServer, senderId, dhPublicKey}) connReq connInfo =
withLogSMP_ c smpServer senderId "SEND <INV>" $ \smp -> do
msg <- mkInvitation
liftSMP $ sendSMPMessage smp Nothing senderId msg
where
mkConfirmation :: SMPClient -> m MsgBody
mkConfirmation smp = encryptAndSign smp sq . serializeSMPMessage $ SMPConfirmation senderKey
mkInvitation :: m ByteString
-- this is only encrypted with per-queue E2E, not with double ratchet
mkInvitation = do
let agentEnvelope = AgentInvitation {agentVersion = smpAgentVersion, connReq, connInfo}
agentCbEncryptOnce dhPublicKey . smpEncode $
SMP.ClientMessage SMP.PHEmpty $ smpEncode agentEnvelope
sendHello :: forall m. AgentMonad m => AgentClient -> SndQueue -> VerificationKey -> m ()
sendHello c sq@SndQueue {server, sndId, sndPrivateKey} verifyKey =
withLogSMP_ c server sndId "SEND <HELLO> (retrying)" $ \smp -> do
msg <- mkHello smp $ AckMode On
liftSMP $ send 8 100000 msg smp
where
mkHello :: SMPClient -> AckMode -> m ByteString
mkHello smp ackMode = do
senderTimestamp <- liftIO getCurrentTime
encryptAndSign smp sq . serializeSMPMessage $
SMPMessage
{ senderMsgId = 0,
senderTimestamp,
previousMsgHash = "",
agentMessage = HELLO verifyKey ackMode
}
send :: Int -> Int -> ByteString -> SMPClient -> ExceptT SMPClientError IO ()
send 0 _ _ _ = throwE $ SMPServerError AUTH
send retry delay msg smp =
sendSMPMessage smp (Just sndPrivateKey) sndId msg `catchE` \case
SMPServerError AUTH -> do
threadDelay delay
send (retry - 1) (delay * 3 `div` 2) msg smp
e -> throwE e
secureQueue :: AgentMonad m => AgentClient -> RcvQueue -> SenderPublicKey -> m ()
secureQueue :: AgentMonad m => AgentClient -> RcvQueue -> SndPublicVerifyKey -> m ()
secureQueue c RcvQueue {server, rcvId, rcvPrivateKey} senderKey =
withLogSMP c server rcvId "KEY <key>" $ \smp ->
secureSMPQueue smp rcvPrivateKey rcvId senderKey
@@ -274,39 +420,48 @@ deleteQueue c RcvQueue {server, rcvId, rcvPrivateKey} =
withLogSMP c server rcvId "DEL" $ \smp ->
deleteSMPQueue smp rcvPrivateKey rcvId
sendAgentMessage :: AgentMonad m => AgentClient -> SndQueue -> ByteString -> m ()
sendAgentMessage c sq@SndQueue {server, sndId, sndPrivateKey} msg =
withLogSMP_ c server sndId "SEND <message>" $ \smp -> do
msg' <- encryptAndSign smp sq msg
liftSMP $ sendSMPMessage smp (Just sndPrivateKey) sndId msg'
sendAgentMessage :: forall m. AgentMonad m => AgentClient -> SndQueue -> ByteString -> m ()
sendAgentMessage c sq@SndQueue {server, sndId, sndPrivateKey} agentMsg =
withLogSMP_ c server sndId "SEND <MSG>" $ \smp -> do
let clientMsg = SMP.ClientMessage SMP.PHEmpty agentMsg
msg <- agentCbEncrypt sq Nothing $ smpEncode clientMsg
liftSMP $ sendSMPMessage smp (Just sndPrivateKey) sndId msg
encryptAndSign :: AgentMonad m => SMPClient -> SndQueue -> ByteString -> m ByteString
encryptAndSign smp SndQueue {encryptKey, signKey} msg = do
paddedSize <- asks $ (blockSize smp -) . reservedMsgSize
liftError cryptoError $ do
enc <- C.encrypt encryptKey paddedSize msg
C.Signature sig <- C.sign signKey enc
pure $ sig <> enc
agentCbEncrypt :: AgentMonad m => SndQueue -> Maybe C.PublicKeyX25519 -> ByteString -> m ByteString
agentCbEncrypt SndQueue {e2eDhSecret} e2ePubKey msg = do
cmNonce <- liftIO C.randomCbNonce
let paddedLen = maybe SMP.e2eEncMessageLength (const SMP.e2eEncConfirmationLength) e2ePubKey
cmEncBody <-
liftEither . first cryptoError $
C.cbEncrypt e2eDhSecret cmNonce msg paddedLen
-- TODO per-queue client version
let cmHeader = SMP.PubHeader (maxVersion SMP.smpClientVRange) e2ePubKey
pure $ smpEncode SMP.ClientMsgEnvelope {cmHeader, cmNonce, cmEncBody}
decryptAndVerify :: AgentMonad m => RcvQueue -> ByteString -> m ByteString
decryptAndVerify RcvQueue {decryptKey, verifyKey} msg =
verifyMessage verifyKey msg
>>= liftError cryptoError . C.decrypt decryptKey
-- add encoding as AgentInvitation'?
agentCbEncryptOnce :: AgentMonad m => C.PublicKeyX25519 -> ByteString -> m ByteString
agentCbEncryptOnce dhRcvPubKey msg = do
(dhSndPubKey, dhSndPrivKey) <- liftIO C.generateKeyPair'
let e2eDhSecret = C.dh' dhRcvPubKey dhSndPrivKey
cmNonce <- liftIO C.randomCbNonce
cmEncBody <-
liftEither . first cryptoError $
C.cbEncrypt e2eDhSecret cmNonce msg SMP.e2eEncConfirmationLength
-- TODO per-queue client version
let cmHeader = SMP.PubHeader (maxVersion SMP.smpClientVRange) (Just dhSndPubKey)
pure $ smpEncode SMP.ClientMsgEnvelope {cmHeader, cmNonce, cmEncBody}
verifyMessage :: AgentMonad m => Maybe VerificationKey -> ByteString -> m ByteString
verifyMessage verifyKey msg = do
size <- asks $ rsaKeySize . config
let (sig, enc) = B.splitAt size msg
case verifyKey of
Nothing -> pure enc
Just k
| C.verify k (C.Signature sig) enc -> pure enc
| otherwise -> throwError $ AGENT A_SIGNATURE
-- | NaCl crypto-box decrypt - both for messages received from the server
-- and per-queue E2E encrypted messages from the sender that were inside.
agentCbDecrypt :: AgentMonad m => C.DhSecretX25519 -> C.CbNonce -> ByteString -> m ByteString
agentCbDecrypt dhSecret nonce msg =
liftEither . first cryptoError $
C.cbDecrypt dhSecret nonce msg
cryptoError :: C.CryptoError -> AgentErrorType
cryptoError = \case
C.CryptoLargeMsgError -> CMD LARGE
C.RSADecryptError _ -> AGENT A_ENCRYPTION
C.CryptoHeaderError _ -> AGENT A_ENCRYPTION
C.AESDecryptError -> AGENT A_ENCRYPTION
C.CBDecryptError -> AGENT A_ENCRYPTION
e -> INTERNAL $ show e
+53 -13
View File
@@ -1,49 +1,89 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE NumericUnderscores #-}
{-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-}
module Simplex.Messaging.Agent.Env.SQLite where
module Simplex.Messaging.Agent.Env.SQLite
( AgentConfig (..),
defaultAgentConfig,
Env (..),
newSMPAgentEnv,
)
where
import Control.Monad.IO.Unlift
import Crypto.Random
import Data.List.NonEmpty (NonEmpty)
import Data.Time.Clock (NominalDiffTime, nominalDay)
import Network.Socket
import Numeric.Natural
import Simplex.Messaging.Agent.Protocol (SMPServer)
import Simplex.Messaging.Agent.RetryInterval
import Simplex.Messaging.Agent.Store.SQLite
import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations
import Simplex.Messaging.Client
import qualified Simplex.Messaging.Crypto as C
import System.Random (StdGen, newStdGen)
import UnliftIO.STM
data AgentConfig = AgentConfig
{ tcpPort :: ServiceName,
smpServers :: NonEmpty SMPServer,
rsaKeySize :: Int,
cmdSignAlg :: C.SignAlg,
connIdBytes :: Int,
tbqSize :: Natural,
dbFile :: FilePath,
smpCfg :: SMPClientConfig
dbPoolSize :: Int,
yesToMigrations :: Bool,
smpCfg :: SMPClientConfig,
reconnectInterval :: RetryInterval,
helloTimeout :: NominalDiffTime,
caCertificateFile :: FilePath,
privateKeyFile :: FilePath,
certificateFile :: FilePath
}
defaultAgentConfig :: AgentConfig
defaultAgentConfig =
AgentConfig
{ tcpPort = "5224",
smpServers = undefined, -- TODO move it elsewhere?
cmdSignAlg = C.SignAlg C.SEd448,
connIdBytes = 12,
tbqSize = 64,
dbFile = "smp-agent.db",
dbPoolSize = 4,
yesToMigrations = False,
smpCfg = smpDefaultConfig,
reconnectInterval =
RetryInterval
{ initialInterval = second,
increaseAfter = 10 * second,
maxInterval = 10 * second
},
helloTimeout = 2 * nominalDay,
-- CA certificate private key is not needed for initialization
-- ! we do not generate these
caCertificateFile = "/etc/opt/simplex-agent/ca.crt",
privateKeyFile = "/etc/opt/simplex-agent/agent.key",
certificateFile = "/etc/opt/simplex-agent/agent.crt"
}
where
second = 1_000_000
data Env = Env
{ config :: AgentConfig,
store :: SQLiteStore,
idsDrg :: TVar ChaChaDRG,
clientCounter :: TVar Int,
reservedMsgSize :: Int,
randomServer :: TVar StdGen
}
newSMPAgentEnv :: (MonadUnliftIO m, MonadRandom m) => AgentConfig -> m Env
newSMPAgentEnv config = do
newSMPAgentEnv config@AgentConfig {dbFile, dbPoolSize, yesToMigrations} = do
idsDrg <- newTVarIO =<< drgNew
_ <- createSQLiteStore $ dbFile config
store <- liftIO $ createSQLiteStore dbFile dbPoolSize Migrations.app yesToMigrations
clientCounter <- newTVarIO 0
randomServer <- newTVarIO =<< liftIO newStdGen
return Env {config, idsDrg, clientCounter, reservedMsgSize, randomServer}
where
-- 1st rsaKeySize is used by the RSA signature in each command,
-- 2nd - by encrypted message body header
-- 3rd - by message signature
-- smpCommandSize - is the estimated max size for SMP command, queueId, corrId
reservedMsgSize = 3 * rsaKeySize config + smpCommandSize (smpCfg config)
return Env {config, store, idsDrg, clientCounter, randomServer}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,30 @@
module Simplex.Messaging.Agent.QueryString where
import Data.Attoparsec.ByteString.Char8 (Parser)
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.List (find)
import qualified Network.HTTP.Types as Q
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (parseAll)
data QueryStringParams = QSP QSPEscaping Q.SimpleQuery
deriving (Show)
data QSPEscaping = QEscape | QNoEscaping
deriving (Show)
instance StrEncoding QueryStringParams where
strEncode (QSP esc q) = case esc of
QEscape -> Q.renderSimpleQuery False q
QNoEscaping ->
Q.renderQueryPartialEscape False $
map (\(n, v) -> (n, [Q.QN v])) q
strP = QSP QEscape . Q.parseSimpleQuery <$> A.takeTill (\c -> c == ' ' || c == '\n')
queryParam :: StrEncoding a => ByteString -> QueryStringParams -> Parser a
queryParam name (QSP _ q) =
case find ((== name) . fst) q of
Just (_, p) -> either fail pure $ parseAll strP p
_ -> fail $ "no qs param " <> B.unpack name
@@ -0,0 +1,28 @@
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Simplex.Messaging.Agent.RetryInterval where
import Control.Concurrent (threadDelay)
import Control.Monad.IO.Class (MonadIO, liftIO)
data RetryInterval = RetryInterval
{ initialInterval :: Int,
increaseAfter :: Int,
maxInterval :: Int
}
withRetryInterval :: forall m. MonadIO m => RetryInterval -> (m () -> m ()) -> m ()
withRetryInterval RetryInterval {initialInterval, increaseAfter, maxInterval} action =
callAction 0 initialInterval
where
callAction :: Int -> Int -> m ()
callAction elapsedTime delay = action loop
where
loop = do
let newDelay =
if elapsedTime < increaseAfter || delay == maxInterval
then delay
else min (delay * 3 `div` 2) maxInterval
liftIO $ threadDelay delay
callAction (elapsedTime + delay) newDelay
+81
View File
@@ -0,0 +1,81 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Simplex.Messaging.Agent.Server
( -- * SMP agent over TCP
runSMPAgent,
runSMPAgentBlocking,
)
where
import Control.Logger.Simple (logInfo)
import Control.Monad.Except
import Control.Monad.IO.Unlift (MonadUnliftIO)
import Control.Monad.Reader
import Crypto.Random (MonadRandom)
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Text.Encoding (decodeUtf8)
import Simplex.Messaging.Agent
import Simplex.Messaging.Agent.Env.SQLite
import Simplex.Messaging.Agent.Protocol
import Simplex.Messaging.Transport (ATransport (..), TProxy, Transport (..), simplexMQVersion)
import Simplex.Messaging.Transport.Server (loadTLSServerParams, runTransportServer)
import Simplex.Messaging.Util (bshow)
import UnliftIO.Async (race_)
import qualified UnliftIO.Exception as E
import UnliftIO.STM
-- | Runs an SMP agent as a TCP service using passed configuration.
--
-- See a full agent executable here: https://github.com/simplex-chat/simplexmq/blob/master/apps/smp-agent/Main.hs
runSMPAgent :: (MonadRandom m, MonadUnliftIO m) => ATransport -> AgentConfig -> m ()
runSMPAgent t cfg = do
started <- newEmptyTMVarIO
runSMPAgentBlocking t started cfg
-- | Runs an SMP agent as a TCP service using passed configuration with signalling.
--
-- This function uses passed TMVar to signal when the server is ready to accept TCP requests (True)
-- and when it is disconnected from the TCP socket once the server thread is killed (False).
runSMPAgentBlocking :: (MonadRandom m, MonadUnliftIO m) => ATransport -> TMVar Bool -> AgentConfig -> m ()
runSMPAgentBlocking (ATransport t) started cfg@AgentConfig {tcpPort, caCertificateFile, certificateFile, privateKeyFile} = do
runReaderT (smpAgent t) =<< newSMPAgentEnv cfg
where
smpAgent :: forall c m'. (Transport c, MonadUnliftIO m', MonadReader Env m') => TProxy c -> m' ()
smpAgent _ = do
-- tlsServerParams is not in Env to avoid breaking functional API w/t key and certificate generation
tlsServerParams <- liftIO $ loadTLSServerParams caCertificateFile certificateFile privateKeyFile
runTransportServer started tcpPort tlsServerParams $ \(h :: c) -> do
liftIO . putLn h $ "Welcome to SMP agent v" <> B.pack simplexMQVersion
c <- getAgentClient
logConnection c True
race_ (connectClient h c) (runAgentClient c)
`E.finally` disconnectAgentClient c
connectClient :: Transport c => MonadUnliftIO m => c -> AgentClient -> m ()
connectClient h c = race_ (send h c) (receive h c)
receive :: forall c m. (Transport c, MonadUnliftIO m) => c -> AgentClient -> m ()
receive h c@AgentClient {rcvQ, subQ} = forever $ do
(corrId, connId, cmdOrErr) <- tGet SClient h
case cmdOrErr of
Right cmd -> write rcvQ (corrId, connId, cmd)
Left e -> write subQ (corrId, connId, ERR e)
where
write :: TBQueue (ATransmission p) -> ATransmission p -> m ()
write q t = do
logClient c "-->" t
atomically $ writeTBQueue q t
send :: (Transport c, MonadUnliftIO m) => c -> AgentClient -> m ()
send h c@AgentClient {subQ} = forever $ do
t <- atomically $ readTBQueue subQ
tPut h t
logClient c "<--" t
logClient :: MonadUnliftIO m => AgentClient -> ByteString -> ATransmission a -> m ()
logClient AgentClient {clientId} dir (corrId, connId, cmd) = do
logInfo . decodeUtf8 $ B.unwords [bshow clientId, dir, "A :", corrId, connId, B.takeWhile (/= ' ') $ serializeCommand cmd]
+128 -109
View File
@@ -9,19 +9,23 @@
module Simplex.Messaging.Agent.Store where
import Control.Concurrent.STM (TVar)
import Control.Exception (Exception)
import Crypto.Random (ChaChaDRG)
import Data.ByteString.Char8 (ByteString)
import Data.Int (Int64)
import Data.Kind (Type)
import Data.Time (UTCTime)
import Data.Type.Equality
import Simplex.Messaging.Agent.Protocol
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.Ratchet (RatchetX448, SkippedMsgDiff, SkippedMsgKeys)
import Simplex.Messaging.Protocol
( MsgBody,
MsgId,
RecipientPrivateKey,
SenderPrivateKey,
SenderPublicKey,
RcvDhSecret,
RcvPrivateSignKey,
SndPrivateSignKey,
)
import qualified Simplex.Messaging.Protocol as SMP
@@ -30,39 +34,65 @@ 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 -> RcvQueue -> m ()
createSndConn :: s -> SndQueue -> m ()
getConn :: s -> ConnAlias -> m SomeConn
getAllConnAliases :: s -> m [ConnAlias] -- TODO remove - hack for subscribing to all
createRcvConn :: s -> TVar ChaChaDRG -> ConnData -> RcvQueue -> SConnectionMode c -> m ConnId
createSndConn :: s -> TVar ChaChaDRG -> ConnData -> SndQueue -> m ConnId
getConn :: s -> ConnId -> m SomeConn
getRcvConn :: s -> SMPServer -> SMP.RecipientId -> m SomeConn
deleteConn :: s -> ConnAlias -> m ()
upgradeRcvConnToDuplex :: s -> ConnAlias -> SndQueue -> m ()
upgradeSndConnToDuplex :: s -> ConnAlias -> RcvQueue -> m ()
deleteConn :: s -> ConnId -> m ()
upgradeRcvConnToDuplex :: s -> ConnId -> SndQueue -> m ()
upgradeSndConnToDuplex :: s -> ConnId -> RcvQueue -> m ()
setRcvQueueStatus :: s -> RcvQueue -> QueueStatus -> m ()
setRcvQueueActive :: s -> RcvQueue -> VerificationKey -> m ()
setRcvQueueConfirmedE2E :: s -> RcvQueue -> C.DhSecretX25519 -> m ()
setSndQueueStatus :: s -> SndQueue -> QueueStatus -> m ()
-- Confirmations
createConfirmation :: s -> TVar ChaChaDRG -> NewConfirmation -> m ConfirmationId
acceptConfirmation :: s -> ConfirmationId -> ConnInfo -> m AcceptedConfirmation
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 ()
deleteInvitation :: s -> ConnId -> InvitationId -> m ()
-- Msg management
updateRcvIds :: s -> RcvQueue -> m (InternalId, InternalRcvId, PrevExternalSndId, PrevRcvMsgHash)
createRcvMsg :: s -> RcvQueue -> RcvMsgData -> m ()
updateRcvIds :: s -> ConnId -> m (InternalId, InternalRcvId, PrevExternalSndId, PrevRcvMsgHash)
createRcvMsg :: s -> ConnId -> RcvMsgData -> m ()
updateSndIds :: s -> ConnId -> m (InternalId, InternalSndId, PrevSndMsgHash)
createSndMsg :: s -> ConnId -> SndMsgData -> m ()
getPendingMsgData :: s -> ConnId -> InternalId -> m (Maybe RcvQueue, (AMsgType, MsgBody, InternalTs))
getPendingMsgs :: s -> ConnId -> m [InternalId]
checkRcvMsg :: s -> ConnId -> InternalId -> m ()
deleteMsg :: s -> ConnId -> InternalId -> m ()
updateSndIds :: s -> SndQueue -> m (InternalId, InternalSndId, PrevSndMsgHash)
createSndMsg :: s -> SndQueue -> SndMsgData -> m ()
getMsg :: s -> ConnAlias -> InternalId -> m Msg
-- Double ratchet persistence
createRatchetX3dhKeys :: s -> ConnId -> C.PrivateKeyX448 -> C.PrivateKeyX448 -> m ()
getRatchetX3dhKeys :: s -> ConnId -> m (C.PrivateKeyX448, C.PrivateKeyX448)
createRatchet :: s -> ConnId -> RatchetX448 -> m ()
getRatchet :: s -> ConnId -> m RatchetX448
getSkippedMsgKeys :: s -> ConnId -> m SkippedMsgKeys
updateRatchet :: s -> ConnId -> RatchetX448 -> SkippedMsgDiff -> m ()
-- * Queue types
-- | A receive queue. SMP queue through which the agent receives messages from a sender.
data RcvQueue = RcvQueue
{ server :: SMPServer,
-- | recipient queue ID
rcvId :: SMP.RecipientId,
connAlias :: ConnAlias,
rcvPrivateKey :: RecipientPrivateKey,
-- | key used by the recipient to sign transmissions
rcvPrivateKey :: RcvPrivateSignKey,
-- | shared DH secret used to encrypt/decrypt message bodies from server to recipient
rcvDhSecret :: RcvDhSecret,
-- | private DH key related to public sent to sender out-of-band (to agree simple per-queue e2e)
e2ePrivKey :: C.PrivateKeyX25519,
-- | public sender's DH key and agreed shared DH secret for simple per-queue e2e
e2eDhSecret :: Maybe C.DhSecretX25519,
-- | sender queue ID
sndId :: Maybe SMP.SenderId,
sndKey :: Maybe SenderPublicKey,
decryptKey :: DecryptionKey,
verifyKey :: Maybe VerificationKey,
-- | queue status
status :: QueueStatus
}
deriving (Eq, Show)
@@ -70,11 +100,13 @@ data RcvQueue = RcvQueue
-- | A send queue. SMP queue through which the agent sends messages to a recipient.
data SndQueue = SndQueue
{ server :: SMPServer,
-- | sender queue ID
sndId :: SMP.SenderId,
connAlias :: ConnAlias,
sndPrivateKey :: SenderPrivateKey,
encryptKey :: EncryptionKey,
signKey :: SignatureKey,
-- | key used by the sender to sign transmissions
sndPrivateKey :: SndPrivateSignKey,
-- | shared DH secret agreed for simple per-queue e2e encryption
e2eDhSecret :: C.DhSecretX25519,
-- | queue status
status :: QueueStatus
}
deriving (Eq, Show)
@@ -82,7 +114,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.
--
@@ -95,9 +127,10 @@ data ConnType = CRcv | CSnd | CDuplex deriving (Eq, Show)
-- - DuplexConnection is a connection that has both receive and send queues set up,
-- typically created by upgrading a receive or a send connection with a missing queue.
data Connection (d :: ConnType) where
RcvConnection :: ConnAlias -> RcvQueue -> Connection CRcv
SndConnection :: ConnAlias -> SndQueue -> Connection CSnd
DuplexConnection :: ConnAlias -> RcvQueue -> SndQueue -> Connection CDuplex
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)
@@ -107,11 +140,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)
@@ -121,6 +156,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.
@@ -134,9 +170,43 @@ instance Eq SomeConn where
deriving instance Show SomeConn
-- * Message integrity validation types
newtype ConnData = ConnData {connId :: ConnId}
deriving (Eq, Show)
type MsgHash = ByteString
-- * Confirmation types
data NewConfirmation = NewConfirmation
{ connId :: ConnId,
senderConf :: SMPConfirmation,
ratchetState :: RatchetX448
}
data AcceptedConfirmation = AcceptedConfirmation
{ confirmationId :: ConfirmationId,
connId :: ConnId,
senderConf :: SMPConfirmation,
ratchetState :: RatchetX448,
ownConnInfo :: ConnInfo
}
-- * Invitations
data NewInvitation = NewInvitation
{ contactConnId :: ConnId,
connReq :: ConnectionRequestUri 'CMInvitation,
recipientConnInfo :: ConnInfo
}
data Invitation = Invitation
{ invitationId :: InvitationId,
contactConnId :: ConnId,
connReq :: ConnectionRequestUri 'CMInvitation,
recipientConnInfo :: ConnInfo,
ownConnInfo :: Maybe ConnInfo,
accepted :: Bool
}
-- * Message integrity validation types
-- | Corresponds to `last_external_snd_msg_id` in `connections` table
type PrevExternalSndId = Int64
@@ -147,62 +217,32 @@ type PrevRcvMsgHash = MsgHash
-- | Corresponds to `last_snd_msg_hash` in `connections` table
type PrevSndMsgHash = MsgHash
-- ? merge/replace these with RcvMsg and SndMsg
-- * Message data containers - used on Msg creation to reduce number of parameters
data RcvMsgData = RcvMsgData
{ internalId :: InternalId,
internalRcvId :: InternalRcvId,
internalTs :: InternalTs,
senderMeta :: (ExternalSndId, ExternalSndTs),
brokerMeta :: (BrokerId, BrokerTs),
{ msgMeta :: MsgMeta,
msgType :: AMsgType,
msgBody :: MsgBody,
internalRcvId :: InternalRcvId,
internalHash :: MsgHash,
externalPrevSndHash :: MsgHash,
msgIntegrity :: MsgIntegrity
externalPrevSndHash :: MsgHash
}
data SndMsgData = SndMsgData
{ internalId :: InternalId,
internalSndId :: InternalSndId,
internalTs :: InternalTs,
msgType :: AMsgType,
msgBody :: MsgBody,
internalHash :: MsgHash
internalHash :: MsgHash,
prevMsgHash :: MsgHash
}
-- * Message types
-- | A message in either direction that is stored by the agent.
data Msg = MRcv RcvMsg | MSnd SndMsg
deriving (Eq, Show)
-- | A message received by the agent from a sender.
data RcvMsg = RcvMsg
{ msgBase :: MsgBase,
internalRcvId :: InternalRcvId,
-- | Id of the message at sender, corresponds to `internalSndId` from the sender's side.
-- Sender Id is made sequential for detection of missing messages. For redundant / parallel queues,
-- it also allows to keep track of duplicates and restore the original order before delivery to the client.
externalSndId :: ExternalSndId,
externalSndTs :: ExternalSndTs,
-- | Id of the message at broker, although it is not sequential (to avoid metadata leakage for potential observer),
-- it is needed to track repeated deliveries in case of connection loss - this logic is not implemented yet.
brokerId :: BrokerId,
brokerTs :: BrokerTs,
rcvMsgStatus :: RcvMsgStatus,
-- | Timestamp of acknowledgement to broker, corresponds to `AcknowledgedToBroker` status.
-- Do not mix up with `brokerTs` - timestamp created at broker after it receives the message from sender.
ackBrokerTs :: AckBrokerTs,
-- | Timestamp of acknowledgement to sender, corresponds to `AcknowledgedToSender` status.
-- Do not mix up with `externalSndTs` - timestamp created at sender before sending,
-- which in its turn corresponds to `internalTs` in sending agent.
ackSenderTs :: AckSenderTs,
-- | Hash of previous message as received from sender - stored for integrity forensics.
externalPrevSndHash :: MsgHash,
msgIntegrity :: MsgIntegrity
data PendingMsg = PendingMsg
{ connId :: ConnId,
msgId :: InternalId
}
deriving (Eq, Show)
deriving (Show)
-- internal Ids are newtypes to prevent mixing them up
newtype InternalRcvId = InternalRcvId {unRcvId :: Int64} deriving (Eq, Show)
@@ -215,44 +255,11 @@ type BrokerId = MsgId
type BrokerTs = UTCTime
data RcvMsgStatus
= Received
| AcknowledgedToBroker
| AcknowledgedToSender
deriving (Eq, Show)
type AckBrokerTs = UTCTime
type AckSenderTs = UTCTime
-- | A message sent by the agent to a recipient.
data SndMsg = SndMsg
{ msgBase :: MsgBase,
-- | Id of the message sent / to be sent, as in its number in order of sending.
internalSndId :: InternalSndId,
sndMsgStatus :: SndMsgStatus,
-- | Timestamp of the message received by broker, corresponds to `Sent` status.
sentTs :: SentTs,
-- | Timestamp of the message received by recipient, corresponds to `Delivered` status.
deliveredTs :: DeliveredTs
}
deriving (Eq, Show)
newtype InternalSndId = InternalSndId {unSndId :: Int64} deriving (Eq, Show)
data SndMsgStatus
= Created
| Sent
| Delivered
deriving (Eq, Show)
type SentTs = UTCTime
type DeliveredTs = UTCTime
-- | Base message data independent of direction.
data MsgBase = MsgBase
{ connAlias :: ConnAlias,
{ connId :: ConnId,
-- | Monotonically increasing id of a message per connection, internal to the agent.
-- Internal Id preserves ordering between both received and sent messages, and is needed
-- to track the order of the conversation (which can be different for the sender / receiver)
@@ -276,17 +283,29 @@ type InternalTs = UTCTime
data StoreError
= -- | IO exceptions in store actions.
SEInternal ByteString
| -- | Connection alias not found (or both queues absent).
| -- | Failed to generate unique random ID
SEUniqueID
| -- | Connection not found (or both queues absent).
SEConnNotFound
| -- | Connection alias already used.
| -- | Connection already used.
SEConnDuplicate
| -- | Wrong connection type, e.g. "send" connection when "receive" or "duplex" is expected, or vice versa.
-- 'upgradeRcvConnToDuplex' and 'upgradeSndConnToDuplex' do not allow duplex connections - they would also return this error.
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,
-- as we always know what it should be at any stage of the protocol,
-- and in case it does not match use this error.
SEBadQueueStatus
| -- | connection does not have associated double-ratchet state
SERatchetNotFound
| -- | connection does not have associated x3dh keys
SEX3dhKeysNotFound
| -- | Used in `getMsg` that is not implemented/used. TODO remove.
SENotImplemented
deriving (Eq, Show, Exception)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,72 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
module Simplex.Messaging.Agent.Store.SQLite.Migrations
( Migration (..),
app,
initialize,
get,
run,
)
where
import Control.Monad (forM_)
import Data.Function (on)
import Data.List (intercalate, sortBy)
import Data.Text (Text)
import Data.Time.Clock (getCurrentTime)
import Database.SQLite.Simple (Connection, Only (..), Query (..))
import qualified Database.SQLite.Simple as DB
import Database.SQLite.Simple.QQ (sql)
import qualified Database.SQLite3 as SQLite3
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220101_initial
data Migration = Migration {name :: String, up :: Text}
deriving (Show)
schemaMigrations :: [(String, Query)]
schemaMigrations =
[ ("20220101_initial", m20220101_initial)
]
-- | The list of migrations in ascending order by date
app :: [Migration]
app = sortBy (compare `on` name) $ map migration schemaMigrations
where
migration (name, query) = Migration {name = name, up = fromQuery query}
get :: Connection -> [Migration] -> IO (Either String [Migration])
get conn migrations =
migrationsToRun migrations . map fromOnly
<$> DB.query_ conn "SELECT name FROM migrations ORDER BY name ASC;"
run :: Connection -> [Migration] -> IO ()
run conn ms = DB.withImmediateTransaction conn . forM_ ms $
\Migration {name, up} -> insert name >> execSQL up
where
insert name = DB.execute conn "INSERT INTO migrations (name, ts) VALUES (?, ?);" . (name,) =<< getCurrentTime
execSQL = SQLite3.exec $ DB.connectionHandle conn
initialize :: Connection -> IO ()
initialize conn =
DB.execute_
conn
[sql|
CREATE TABLE IF NOT EXISTS migrations (
name TEXT NOT NULL,
ts TEXT NOT NULL,
PRIMARY KEY (name)
);
|]
migrationsToRun :: [Migration] -> [String] -> Either String [Migration]
migrationsToRun appMs [] = Right appMs
migrationsToRun [] dbMs = Left $ "database version is newer than the app: " <> intercalate ", " dbMs
migrationsToRun (a : as) (d : ds)
| name a == d = migrationsToRun as ds
| otherwise = Left $ "different migration in the app/database: " <> name a <> " / " <> d
@@ -0,0 +1,148 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220101_initial where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20220101_initial :: Query
m20220101_initial =
[sql|
CREATE TABLE servers (
host TEXT NOT NULL,
port TEXT NOT NULL,
key_hash BLOB NOT NULL,
PRIMARY KEY (host, port)
) WITHOUT ROWID;
CREATE TABLE connections (
conn_id BLOB NOT NULL PRIMARY KEY,
conn_mode TEXT NOT NULL,
last_internal_msg_id INTEGER NOT NULL DEFAULT 0,
last_internal_rcv_msg_id INTEGER NOT NULL DEFAULT 0,
last_internal_snd_msg_id INTEGER NOT NULL DEFAULT 0,
last_external_snd_msg_id INTEGER NOT NULL DEFAULT 0,
last_rcv_msg_hash BLOB NOT NULL DEFAULT x'',
last_snd_msg_hash BLOB NOT NULL DEFAULT x'',
smp_agent_version INTEGER NOT NULL DEFAULT 1
) WITHOUT ROWID;
CREATE TABLE rcv_queues (
host TEXT NOT NULL,
port TEXT NOT NULL,
rcv_id BLOB NOT NULL,
conn_id BLOB NOT NULL REFERENCES connections ON DELETE CASCADE,
rcv_private_key BLOB NOT NULL,
rcv_dh_secret BLOB NOT NULL,
e2e_priv_key BLOB NOT NULL,
e2e_dh_secret BLOB,
snd_id BLOB NOT NULL,
snd_key BLOB,
status TEXT NOT NULL,
smp_server_version INTEGER NOT NULL DEFAULT 1,
smp_client_version INTEGER,
PRIMARY KEY (host, port, rcv_id),
FOREIGN KEY (host, port) REFERENCES servers
ON DELETE RESTRICT ON UPDATE CASCADE,
UNIQUE (host, port, snd_id)
) WITHOUT ROWID;
CREATE TABLE snd_queues (
host TEXT NOT NULL,
port TEXT NOT NULL,
snd_id BLOB NOT NULL,
conn_id BLOB NOT NULL REFERENCES connections ON DELETE CASCADE,
snd_private_key BLOB NOT NULL,
e2e_dh_secret BLOB NOT NULL,
status TEXT NOT NULL,
smp_server_version INTEGER NOT NULL DEFAULT 1,
smp_client_version INTEGER NOT NULL DEFAULT 1,
PRIMARY KEY (host, port, snd_id),
FOREIGN KEY (host, port) REFERENCES servers
ON DELETE RESTRICT ON UPDATE CASCADE
) WITHOUT ROWID;
CREATE TABLE messages (
conn_id BLOB NOT NULL REFERENCES connections (conn_id)
ON DELETE CASCADE,
internal_id INTEGER NOT NULL,
internal_ts TEXT NOT NULL,
internal_rcv_id INTEGER,
internal_snd_id INTEGER,
msg_type BLOB NOT NULL, -- (H)ELLO, (R)EPLY, (D)ELETE. Should SMP confirmation be saved too?
msg_body BLOB NOT NULL DEFAULT x'',
PRIMARY KEY (conn_id, internal_id),
FOREIGN KEY (conn_id, internal_rcv_id) REFERENCES rcv_messages
ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
FOREIGN KEY (conn_id, internal_snd_id) REFERENCES snd_messages
ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED
) WITHOUT ROWID;
CREATE TABLE rcv_messages (
conn_id BLOB NOT NULL,
internal_rcv_id INTEGER NOT NULL,
internal_id INTEGER NOT NULL,
external_snd_id INTEGER NOT NULL,
broker_id BLOB NOT NULL,
broker_ts TEXT NOT NULL,
internal_hash BLOB NOT NULL,
external_prev_snd_hash BLOB NOT NULL,
integrity BLOB NOT NULL,
PRIMARY KEY (conn_id, internal_rcv_id),
FOREIGN KEY (conn_id, internal_id) REFERENCES messages
ON DELETE CASCADE
) WITHOUT ROWID;
CREATE TABLE snd_messages (
conn_id BLOB NOT NULL,
internal_snd_id INTEGER NOT NULL,
internal_id INTEGER NOT NULL,
internal_hash BLOB NOT NULL,
previous_msg_hash BLOB NOT NULL DEFAULT x'',
PRIMARY KEY (conn_id, internal_snd_id),
FOREIGN KEY (conn_id, internal_id) REFERENCES messages
ON DELETE CASCADE
) WITHOUT ROWID;
CREATE TABLE conn_confirmations (
confirmation_id BLOB NOT NULL PRIMARY KEY,
conn_id BLOB NOT NULL REFERENCES connections ON DELETE CASCADE,
e2e_snd_pub_key BLOB NOT NULL, -- TODO per-queue key. Split?
sender_key BLOB NOT NULL, -- TODO per-queue key. Split?
ratchet_state BLOB NOT NULL,
sender_conn_info BLOB NOT NULL,
accepted INTEGER NOT NULL,
own_conn_info BLOB,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
) WITHOUT ROWID;
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;
CREATE TABLE ratchets (
conn_id BLOB NOT NULL PRIMARY KEY REFERENCES connections
ON DELETE CASCADE,
-- x3dh keys are not saved on the sending side (the side accepting the connection)
x3dh_priv_key_1 BLOB,
x3dh_priv_key_2 BLOB,
-- ratchet is initially empty on the receiving side (the side offering the connection)
ratchet_state BLOB,
e2e_version INTEGER NOT NULL DEFAULT 1
) WITHOUT ROWID;
CREATE TABLE skipped_messages (
skipped_message_id INTEGER PRIMARY KEY,
conn_id BLOB NOT NULL REFERENCES ratchets
ON DELETE CASCADE,
header_key BLOB NOT NULL,
msg_n INTEGER NOT NULL,
msg_key BLOB NOT NULL
);
|]
@@ -1,167 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Messaging.Agent.Store.SQLite.Schema (createSchema) where
import Database.SQLite.Simple (Connection, Query, execute_)
import Database.SQLite.Simple.QQ (sql)
createSchema :: Connection -> IO ()
createSchema conn =
mapM_
(execute_ conn)
[ servers,
rcvQueues,
sndQueues,
connections,
messages,
rcvMessages,
sndMessages
]
-- port is either a port number or a service name, see Network.Socket.Info.ServiceName
servers :: Query
servers =
[sql|
CREATE TABLE IF NOT EXISTS servers(
host TEXT NOT NULL,
port TEXT NOT NULL,
key_hash BLOB,
PRIMARY KEY (host, port)
) WITHOUT ROWID;
|]
rcvQueues :: Query
rcvQueues =
[sql|
CREATE TABLE IF NOT EXISTS rcv_queues(
host TEXT NOT NULL,
port TEXT NOT NULL,
rcv_id BLOB NOT NULL,
conn_alias BLOB NOT NULL,
rcv_private_key BLOB NOT NULL,
snd_id BLOB,
snd_key BLOB,
decrypt_key BLOB NOT NULL,
verify_key BLOB,
status TEXT NOT NULL,
PRIMARY KEY (host, port, rcv_id),
FOREIGN KEY (host, port) REFERENCES servers (host, port),
FOREIGN KEY (conn_alias)
REFERENCES connections (conn_alias)
ON DELETE CASCADE
DEFERRABLE INITIALLY DEFERRED,
UNIQUE (host, port, snd_id)
) WITHOUT ROWID;
|]
sndQueues :: Query
sndQueues =
[sql|
CREATE TABLE IF NOT EXISTS snd_queues(
host TEXT NOT NULL,
port TEXT NOT NULL,
snd_id BLOB NOT NULL,
conn_alias BLOB NOT NULL,
snd_private_key BLOB NOT NULL,
encrypt_key BLOB NOT NULL,
sign_key BLOB NOT NULL,
status TEXT NOT NULL,
PRIMARY KEY (host, port, snd_id),
FOREIGN KEY (host, port) REFERENCES servers (host, port),
FOREIGN KEY (conn_alias)
REFERENCES connections (conn_alias)
ON DELETE CASCADE
DEFERRABLE INITIALLY DEFERRED
) WITHOUT ROWID;
|]
connections :: Query
connections =
[sql|
CREATE TABLE IF NOT EXISTS connections(
conn_alias BLOB NOT NULL,
rcv_host TEXT,
rcv_port TEXT,
rcv_id BLOB,
snd_host TEXT,
snd_port TEXT,
snd_id BLOB,
last_internal_msg_id INTEGER NOT NULL,
last_internal_rcv_msg_id INTEGER NOT NULL,
last_internal_snd_msg_id INTEGER NOT NULL,
last_external_snd_msg_id INTEGER NOT NULL,
last_rcv_msg_hash BLOB NOT NULL,
last_snd_msg_hash BLOB NOT NULL,
PRIMARY KEY (conn_alias),
FOREIGN KEY (rcv_host, rcv_port, rcv_id) REFERENCES rcv_queues (host, port, rcv_id),
FOREIGN KEY (snd_host, snd_port, snd_id) REFERENCES snd_queues (host, port, snd_id)
) WITHOUT ROWID;
|]
messages :: Query
messages =
[sql|
CREATE TABLE IF NOT EXISTS messages(
conn_alias BLOB NOT NULL,
internal_id INTEGER NOT NULL,
internal_ts TEXT NOT NULL,
internal_rcv_id INTEGER,
internal_snd_id INTEGER,
body TEXT NOT NULL,
PRIMARY KEY (conn_alias, internal_id),
FOREIGN KEY (conn_alias)
REFERENCES connections (conn_alias)
ON DELETE CASCADE,
FOREIGN KEY (conn_alias, internal_rcv_id)
REFERENCES rcv_messages (conn_alias, internal_rcv_id)
ON DELETE CASCADE
DEFERRABLE INITIALLY DEFERRED,
FOREIGN KEY (conn_alias, internal_snd_id)
REFERENCES snd_messages (conn_alias, internal_snd_id)
ON DELETE CASCADE
DEFERRABLE INITIALLY DEFERRED
) WITHOUT ROWID;
|]
rcvMessages :: Query
rcvMessages =
[sql|
CREATE TABLE IF NOT EXISTS rcv_messages(
conn_alias BLOB NOT NULL,
internal_rcv_id INTEGER NOT NULL,
internal_id INTEGER NOT NULL,
external_snd_id INTEGER NOT NULL,
external_snd_ts TEXT NOT NULL,
broker_id BLOB NOT NULL,
broker_ts TEXT NOT NULL,
rcv_status TEXT NOT NULL,
ack_brocker_ts TEXT,
ack_sender_ts TEXT,
internal_hash BLOB NOT NULL,
external_prev_snd_hash BLOB NOT NULL,
integrity BLOB NOT NULL,
PRIMARY KEY (conn_alias, internal_rcv_id),
FOREIGN KEY (conn_alias, internal_id)
REFERENCES messages (conn_alias, internal_id)
ON DELETE CASCADE
) WITHOUT ROWID;
|]
sndMessages :: Query
sndMessages =
[sql|
CREATE TABLE IF NOT EXISTS snd_messages(
conn_alias BLOB NOT NULL,
internal_snd_id INTEGER NOT NULL,
internal_id INTEGER NOT NULL,
snd_status TEXT NOT NULL,
sent_ts TEXT,
delivered_ts TEXT,
internal_hash BLOB NOT NULL,
PRIMARY KEY (conn_alias, internal_snd_id),
FOREIGN KEY (conn_alias, internal_id)
REFERENCES messages (conn_alias, internal_id)
ON DELETE CASCADE
) WITHOUT ROWID;
|]
+106 -85
View File
@@ -23,14 +23,16 @@
-- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md
module Simplex.Messaging.Client
( -- * Connect (disconnect) client to (from) SMP server
SMPClient (blockSize),
SMPClient,
getSMPClient,
closeSMPClient,
-- * SMP protocol command functions
createSMPQueue,
subscribeSMPQueue,
subscribeSMPQueueNotifications,
secureSMPQueue,
enableSMPQueueNotifications,
sendSMPMessage,
ackSMPMessage,
suspendSMPQueue,
@@ -53,18 +55,19 @@ import Control.Monad
import Control.Monad.Trans.Class
import Control.Monad.Trans.Except
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Data.Maybe
import Data.Maybe (fromMaybe)
import Network.Socket (ServiceName)
import Numeric.Natural
import Simplex.Messaging.Agent.Protocol (SMPServer (..))
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Protocol
import Simplex.Messaging.Transport (ATransport (..), TCP, THandle (..), TProxy, Transport (..), TransportError, clientHandshake, runTransportClient)
import Simplex.Messaging.Transport (ATransport (..), THandle (..), TLS, TProxy, Transport (..), TransportError, clientHandshake)
import Simplex.Messaging.Transport.Client (runTransportClient)
import Simplex.Messaging.Transport.WebSockets (WS)
import Simplex.Messaging.Util (bshow, liftError, raceAny_)
import System.Timeout
import System.Timeout (timeout)
-- | 'SMPClient' is a handle used to send commands to a specific SMP server.
--
@@ -75,18 +78,18 @@ import System.Timeout
data SMPClient = SMPClient
{ action :: Async (),
connected :: TVar Bool,
sessionId :: ByteString,
smpServer :: SMPServer,
tcpTimeout :: Int,
clientCorrId :: TVar Natural,
sentCommands :: TVar (Map CorrId Request),
sndQ :: TBQueue SignedRawTransmission,
rcvQ :: TBQueue SignedTransmissionOrError,
msgQ :: TBQueue SMPServerTransmission,
blockSize :: Int
sndQ :: TBQueue SentRawTransmission,
rcvQ :: TBQueue (SignedTransmission BrokerMsg),
msgQ :: TBQueue SMPServerTransmission
}
-- | Type synonym for transmission from some SPM server queue.
type SMPServerTransmission = (SMPServer, RecipientId, Command 'Broker)
type SMPServerTransmission = (SMPServer, RecipientId, BrokerMsg)
-- | SMP client configuration.
data SMPClientConfig = SMPClientConfig
@@ -97,21 +100,17 @@ data SMPClientConfig = SMPClientConfig
-- | timeout of TCP commands (microseconds)
tcpTimeout :: Int,
-- | period for SMP ping commands (microseconds)
smpPing :: Int,
-- | estimated maximum size of SMP command excluding message body,
-- determines the maximum allowed message size
smpCommandSize :: Int
smpPing :: Int
}
-- | Default SMP client configuration.
smpDefaultConfig :: SMPClientConfig
smpDefaultConfig =
SMPClientConfig
{ qSize = 16,
defaultTransport = ("5223", transport @TCP),
{ qSize = 64,
defaultTransport = ("5223", transport @TLS),
tcpTimeout = 4_000_000,
smpPing = 30_000_000,
smpCommandSize = 256
smpPing = 30_000_000
}
data Request = Request
@@ -119,7 +118,7 @@ data Request = Request
responseVar :: TMVar Response
}
type Response = Either SMPClientError Cmd
type Response = Either SMPClientError BrokerMsg
-- | Connects to 'SMPServer' using passed client configuration
-- and queue for messages and notifications.
@@ -140,7 +139,7 @@ getSMPClient smpServer cfg@SMPClientConfig {qSize, tcpTimeout, smpPing} msgQ dis
return
SMPClient
{ action = undefined,
blockSize = undefined,
sessionId = undefined,
connected,
smpServer,
tcpTimeout,
@@ -156,61 +155,67 @@ getSMPClient smpServer cfg@SMPClientConfig {qSize, tcpTimeout, smpPing} msgQ dis
thVar <- newEmptyTMVarIO
action <-
async $
runTransportClient (host smpServer) port' (client t c thVar)
runTransportClient (host smpServer) port' (keyHash smpServer) (client t c thVar)
`finally` atomically (putTMVar thVar $ Left SMPNetworkError)
bSize <- tcpTimeout `timeout` atomically (takeTMVar thVar)
pure $ case bSize of
Just (Right blockSize) -> Right c {action, blockSize}
th_ <- tcpTimeout `timeout` atomically (takeTMVar thVar)
pure $ case th_ of
Just (Right THandle {sessionId}) -> Right c {action, sessionId}
Just (Left e) -> Left e
Nothing -> Left SMPNetworkError
useTransport :: (ServiceName, ATransport)
useTransport = case port smpServer of
Nothing -> defaultTransport cfg
Just "80" -> ("80", transport @WS)
Just p -> (p, transport @TCP)
"" -> defaultTransport cfg
"80" -> ("80", transport @WS)
p -> (p, transport @TLS)
client :: forall c. Transport c => TProxy c -> SMPClient -> TMVar (Either SMPClientError Int) -> c -> IO ()
client :: forall c. Transport c => TProxy c -> SMPClient -> TMVar (Either SMPClientError (THandle c)) -> c -> IO ()
client _ c thVar h =
runExceptT (clientHandshake h $ keyHash smpServer) >>= \case
Left e -> atomically . putTMVar thVar . Left $ SMPTransportError e
Right th -> do
Right th@THandle {sessionId} -> do
atomically $ do
writeTVar (connected c) True
putTMVar thVar . Right $ blockSize (th :: THandle c)
raceAny_ [send c th, process c, receive c th, ping c]
putTMVar thVar $ Right th
let c' = c {sessionId} :: SMPClient
raceAny_ [send c' th, process c', receive c' th, ping c']
`finally` disconnected
send :: Transport c => SMPClient -> THandle c -> IO ()
send SMPClient {sndQ} h = forever $ atomically (readTBQueue sndQ) >>= tPut h
receive :: Transport c => SMPClient -> THandle c -> IO ()
receive SMPClient {rcvQ} h = forever $ tGet fromServer h >>= atomically . writeTBQueue rcvQ
receive SMPClient {rcvQ} h = forever $ tGet h >>= atomically . writeTBQueue rcvQ
ping :: SMPClient -> IO ()
ping c = forever $ do
threadDelay smpPing
runExceptT $ sendSMPCommand c Nothing "" (Cmd SSender PING)
runExceptT $ sendSMPCommand c Nothing "" PING
process :: SMPClient -> IO ()
process SMPClient {rcvQ, sentCommands} = forever $ do
(_, (corrId, qId, respOrErr)) <- atomically $ readTBQueue rcvQ
cs <- readTVarIO sentCommands
case M.lookup corrId cs of
Nothing -> do
case respOrErr of
Right (Cmd SBroker cmd) -> atomically $ writeTBQueue msgQ (smpServer, qId, cmd)
-- TODO send everything else to errQ and log in agent
_ -> return ()
Just Request {queueId, responseVar} -> atomically $ do
modifyTVar sentCommands $ M.delete corrId
putTMVar responseVar $
if queueId == qId
then case respOrErr of
Left e -> Left $ SMPResponseError e
Right (Cmd _ (ERR e)) -> Left $ SMPServerError e
Right r -> Right r
else Left SMPUnexpectedResponse
(_, _, (corrId, qId, respOrErr)) <- atomically $ readTBQueue rcvQ
if B.null $ bs corrId
then sendMsg qId respOrErr
else do
cs <- readTVarIO sentCommands
case M.lookup corrId cs of
Nothing -> sendMsg qId respOrErr
Just Request {queueId, responseVar} -> atomically $ do
modifyTVar sentCommands $ M.delete corrId
putTMVar responseVar $
if queueId == qId
then case respOrErr of
Left e -> Left $ SMPResponseError e
Right (ERR e) -> Left $ SMPServerError e
Right r -> Right r
else Left SMPUnexpectedResponse
sendMsg :: QueueId -> Either ErrorType BrokerMsg -> IO ()
sendMsg qId = \case
Right cmd -> atomically $ writeTBQueue msgQ (smpServer, qId, cmd)
-- TODO send everything else to errQ and log in agent
_ -> return ()
-- | Disconnects SMP client from the server and terminates client threads.
closeSMPClient :: SMPClient -> IO ()
@@ -247,49 +252,64 @@ data SMPClientError
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#create-queue-command
createSMPQueue ::
SMPClient ->
RecipientPrivateKey ->
RecipientPublicKey ->
ExceptT SMPClientError IO (RecipientId, SenderId)
createSMPQueue c rpKey rKey =
-- TODO add signing this request too - requires changes in the server
sendSMPCommand c (Just rpKey) "" (Cmd SRecipient $ NEW rKey) >>= \case
Cmd _ (IDS rId sId) -> return (rId, sId)
RcvPrivateSignKey ->
RcvPublicVerifyKey ->
RcvPublicDhKey ->
ExceptT SMPClientError IO QueueIdsKeys
createSMPQueue c rpKey rKey dhKey =
sendSMPCommand c (Just rpKey) "" (NEW rKey dhKey) >>= \case
IDS qik -> pure qik
_ -> throwE SMPUnexpectedResponse
-- | Subscribe to the SMP queue.
--
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#subscribe-to-queue
subscribeSMPQueue :: SMPClient -> RecipientPrivateKey -> RecipientId -> ExceptT SMPClientError IO ()
subscribeSMPQueue :: SMPClient -> RcvPrivateSignKey -> RecipientId -> ExceptT SMPClientError IO ()
subscribeSMPQueue c@SMPClient {smpServer, msgQ} rpKey rId =
sendSMPCommand c (Just rpKey) rId (Cmd SRecipient SUB) >>= \case
Cmd _ OK -> return ()
Cmd _ cmd@MSG {} ->
sendSMPCommand c (Just rpKey) rId SUB >>= \case
OK -> return ()
cmd@MSG {} ->
lift . atomically $ writeTBQueue msgQ (smpServer, rId, cmd)
_ -> throwE SMPUnexpectedResponse
-- | Subscribe to the SMP queue notifications.
--
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#subscribe-to-queue-notifications
subscribeSMPQueueNotifications :: SMPClient -> NtfPrivateSignKey -> NotifierId -> ExceptT SMPClientError IO ()
subscribeSMPQueueNotifications = okSMPCommand NSUB
-- | Secure the SMP queue by adding a sender public key.
--
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#secure-queue-command
secureSMPQueue :: SMPClient -> RecipientPrivateKey -> RecipientId -> SenderPublicKey -> ExceptT SMPClientError IO ()
secureSMPQueue c rpKey rId senderKey = okSMPCommand (Cmd SRecipient $ KEY senderKey) c rpKey rId
secureSMPQueue :: SMPClient -> RcvPrivateSignKey -> RecipientId -> SndPublicVerifyKey -> ExceptT SMPClientError IO ()
secureSMPQueue c rpKey rId senderKey = okSMPCommand (KEY senderKey) c rpKey rId
-- | Enable notifications for the queue for push notifications server.
--
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#enable-notifications-command
enableSMPQueueNotifications :: SMPClient -> RcvPrivateSignKey -> RecipientId -> NtfPublicVerifyKey -> ExceptT SMPClientError IO NotifierId
enableSMPQueueNotifications c rpKey rId notifierKey =
sendSMPCommand c (Just rpKey) rId (NKEY notifierKey) >>= \case
NID nId -> pure nId
_ -> throwE SMPUnexpectedResponse
-- | Send SMP message.
--
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#send-message
sendSMPMessage :: SMPClient -> Maybe SenderPrivateKey -> SenderId -> MsgBody -> ExceptT SMPClientError IO ()
sendSMPMessage :: SMPClient -> Maybe SndPrivateSignKey -> SenderId -> MsgBody -> ExceptT SMPClientError IO ()
sendSMPMessage c spKey sId msg =
sendSMPCommand c spKey sId (Cmd SSender $ SEND msg) >>= \case
Cmd _ OK -> return ()
sendSMPCommand c spKey sId (SEND msg) >>= \case
OK -> pure ()
_ -> throwE SMPUnexpectedResponse
-- | Acknowledge message delivery (server deletes the message).
--
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#acknowledge-message-delivery
ackSMPMessage :: SMPClient -> RecipientPrivateKey -> QueueId -> ExceptT SMPClientError IO ()
ackSMPMessage :: SMPClient -> RcvPrivateSignKey -> QueueId -> ExceptT SMPClientError IO ()
ackSMPMessage c@SMPClient {smpServer, msgQ} rpKey rId =
sendSMPCommand c (Just rpKey) rId (Cmd SRecipient ACK) >>= \case
Cmd _ OK -> return ()
Cmd _ cmd@MSG {} ->
sendSMPCommand c (Just rpKey) rId ACK >>= \case
OK -> return ()
cmd@MSG {} ->
lift . atomically $ writeTBQueue msgQ (smpServer, rId, cmd)
_ -> throwE SMPUnexpectedResponse
@@ -297,26 +317,27 @@ ackSMPMessage c@SMPClient {smpServer, msgQ} rpKey rId =
-- The existing messages from the queue will still be delivered.
--
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#suspend-queue
suspendSMPQueue :: SMPClient -> RecipientPrivateKey -> QueueId -> ExceptT SMPClientError IO ()
suspendSMPQueue = okSMPCommand $ Cmd SRecipient OFF
suspendSMPQueue :: SMPClient -> RcvPrivateSignKey -> QueueId -> ExceptT SMPClientError IO ()
suspendSMPQueue = okSMPCommand OFF
-- | Irreversibly delete SMP queue and all messages in it.
--
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#delete-queue
deleteSMPQueue :: SMPClient -> RecipientPrivateKey -> QueueId -> ExceptT SMPClientError IO ()
deleteSMPQueue = okSMPCommand $ Cmd SRecipient DEL
deleteSMPQueue :: SMPClient -> RcvPrivateSignKey -> QueueId -> ExceptT SMPClientError IO ()
deleteSMPQueue = okSMPCommand DEL
okSMPCommand :: Cmd -> SMPClient -> C.SafePrivateKey -> QueueId -> ExceptT SMPClientError IO ()
okSMPCommand :: PartyI p => Command p -> SMPClient -> C.APrivateSignKey -> QueueId -> ExceptT SMPClientError IO ()
okSMPCommand cmd c pKey qId =
sendSMPCommand c (Just pKey) qId cmd >>= \case
Cmd _ OK -> return ()
OK -> return ()
_ -> throwE SMPUnexpectedResponse
-- | Send any SMP command ('Cmd' type).
sendSMPCommand :: SMPClient -> Maybe C.SafePrivateKey -> QueueId -> Cmd -> ExceptT SMPClientError IO Cmd
sendSMPCommand SMPClient {sndQ, sentCommands, clientCorrId, tcpTimeout} pKey qId cmd = do
-- | Send SMP command
-- TODO sign all requests (SEND of SMP confirmation would be signed with the same key that is passed to the recipient)
sendSMPCommand :: PartyI p => SMPClient -> Maybe C.APrivateSignKey -> QueueId -> Command p -> ExceptT SMPClientError IO BrokerMsg
sendSMPCommand SMPClient {sndQ, sentCommands, clientCorrId, sessionId, tcpTimeout} pKey qId cmd = do
corrId <- lift_ getNextCorrId
t <- signTransmission $ serializeTransmission (corrId, qId, cmd)
t <- signTransmission $ encodeTransmission sessionId (corrId, qId, cmd)
ExceptT $ sendRecv corrId t
where
lift_ :: STM a -> ExceptT SMPClientError IO a
@@ -327,20 +348,20 @@ sendSMPCommand SMPClient {sndQ, sentCommands, clientCorrId, tcpTimeout} pKey qId
i <- stateTVar clientCorrId $ \i -> (i, i + 1)
pure . CorrId $ bshow i
signTransmission :: ByteString -> ExceptT SMPClientError IO SignedRawTransmission
signTransmission :: ByteString -> ExceptT SMPClientError IO SentRawTransmission
signTransmission t = case pKey of
Nothing -> return ("", t)
Nothing -> return (Nothing, t)
Just pk -> do
sig <- liftError SMPSignatureError $ C.sign pk t
return (sig, t)
return (Just sig, t)
-- two separate "atomically" needed to avoid blocking
sendRecv :: CorrId -> SignedRawTransmission -> IO Response
sendRecv :: CorrId -> SentRawTransmission -> IO Response
sendRecv corrId t = atomically (send corrId t) >>= withTimeout . atomically . takeTMVar
where
withTimeout a = fromMaybe (Left SMPResponseTimeout) <$> timeout tcpTimeout a
send :: CorrId -> SignedRawTransmission -> STM (TMVar Response)
send :: CorrId -> SentRawTransmission -> STM (TMVar Response)
send corrId t = do
r <- newEmptyTMVar
modifyTVar sentCommands . M.insert corrId $ Request qId r
File diff suppressed because it is too large Load Diff
+488
View File
@@ -0,0 +1,488 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}
module Simplex.Messaging.Crypto.Ratchet where
import Control.Monad.Except
import Control.Monad.Trans.Except
import Crypto.Cipher.AES (AES256)
import Crypto.Hash (SHA512)
import qualified Crypto.KDF.HKDF as H
import Data.Aeson (FromJSON, ToJSON)
import qualified Data.Aeson as J
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy as LB
import qualified Data.List.NonEmpty as L
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Data.Maybe (fromMaybe)
import Data.Typeable (Typeable)
import Data.Word (Word32)
import Database.SQLite.Simple.FromField (FromField (..))
import Database.SQLite.Simple.ToField (ToField (..))
import GHC.Generics
import Simplex.Messaging.Agent.QueryString
import Simplex.Messaging.Crypto
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (blobFieldDecoder, parseE, parseE')
import Simplex.Messaging.Util (tryE)
import Simplex.Messaging.Version
e2eEncryptVersion :: Version
e2eEncryptVersion = 1
e2eEncryptVRange :: VersionRange
e2eEncryptVRange = mkVersionRange 1 e2eEncryptVersion
data E2ERatchetParams (a :: Algorithm)
= E2ERatchetParams Version (PublicKey a) (PublicKey a)
deriving (Eq, Show)
instance AlgorithmI a => Encoding (E2ERatchetParams a) where
smpEncode (E2ERatchetParams v k1 k2) = smpEncode (v, k1, k2)
smpP = E2ERatchetParams <$> smpP <*> smpP <*> smpP
instance VersionI (E2ERatchetParams a) where
type VersionRangeT (E2ERatchetParams a) = E2ERatchetParamsUri a
version (E2ERatchetParams v _ _) = v
toVersionRangeT (E2ERatchetParams _ k1 k2) vr = E2ERatchetParamsUri vr k1 k2
instance VersionRangeI (E2ERatchetParamsUri a) where
type VersionT (E2ERatchetParamsUri a) = (E2ERatchetParams a)
versionRange (E2ERatchetParamsUri vr _ _) = vr
toVersionT (E2ERatchetParamsUri _ k1 k2) v = E2ERatchetParams v k1 k2
data E2ERatchetParamsUri (a :: Algorithm)
= E2ERatchetParamsUri VersionRange (PublicKey a) (PublicKey a)
deriving (Eq, Show)
instance AlgorithmI a => StrEncoding (E2ERatchetParamsUri a) where
strEncode (E2ERatchetParamsUri vs key1 key2) =
strEncode $
QSP QNoEscaping [("v", strEncode vs), ("x3dh", strEncodeList [key1, key2])]
strP = do
query <- strP
vs <- queryParam "v" query
keys <- L.toList <$> queryParam "x3dh" query
case keys of
[key1, key2] -> pure $ E2ERatchetParamsUri vs key1 key2
_ -> fail "bad e2e params"
generateE2EParams :: (AlgorithmI a, DhAlgorithm a) => Version -> IO (PrivateKey a, PrivateKey a, E2ERatchetParams a)
generateE2EParams v = do
(k1, pk1) <- generateKeyPair'
(k2, pk2) <- generateKeyPair'
pure (pk1, pk2, E2ERatchetParams v k1 k2)
data RatchetInitParams = RatchetInitParams
{ assocData :: Str,
ratchetKey :: RatchetKey,
sndHK :: HeaderKey,
rcvNextHK :: HeaderKey
}
deriving (Eq, Show)
x3dhSnd :: DhAlgorithm a => PrivateKey a -> PrivateKey a -> E2ERatchetParams a -> RatchetInitParams
x3dhSnd spk1 spk2 (E2ERatchetParams _ rk1 rk2) =
x3dh (publicKey spk1, rk1) (dh' rk1 spk2) (dh' rk2 spk1) (dh' rk2 spk2)
x3dhRcv :: DhAlgorithm a => PrivateKey a -> PrivateKey a -> E2ERatchetParams a -> RatchetInitParams
x3dhRcv rpk1 rpk2 (E2ERatchetParams _ sk1 sk2) =
x3dh (sk1, publicKey rpk1) (dh' sk2 rpk1) (dh' sk1 rpk2) (dh' sk2 rpk2)
x3dh :: DhAlgorithm a => (PublicKey a, PublicKey a) -> DhSecret a -> DhSecret a -> DhSecret a -> RatchetInitParams
x3dh (sk1, rk1) dh1 dh2 dh3 =
RatchetInitParams {assocData, ratchetKey = RatchetKey sk, sndHK = Key hk, rcvNextHK = Key nhk}
where
assocData = Str $ pubKeyBytes sk1 <> pubKeyBytes rk1
(hk, rest) = B.splitAt 32 $ dhBytes' dh1 <> dhBytes' dh2 <> dhBytes' dh3
(nhk, sk) = B.splitAt 32 rest
type RatchetX448 = Ratchet 'X448
data Ratchet a = Ratchet
{ -- ratchet version range sent in messages (current .. max supported ratchet version)
rcVersion :: VersionRange,
-- associated data - must be the same in both parties ratchets
rcAD :: Str,
rcDHRs :: PrivateKey a,
rcRK :: RatchetKey,
rcSnd :: Maybe (SndRatchet a),
rcRcv :: Maybe RcvRatchet,
rcNs :: Word32,
rcNr :: Word32,
rcPN :: Word32,
rcNHKs :: HeaderKey,
rcNHKr :: HeaderKey
}
deriving (Eq, Show, Generic, FromJSON)
instance AlgorithmI a => ToJSON (Ratchet a) where
toEncoding = J.genericToEncoding J.defaultOptions
data SndRatchet a = SndRatchet
{ rcDHRr :: PublicKey a,
rcCKs :: RatchetKey,
rcHKs :: HeaderKey
}
deriving (Eq, Show, Generic, FromJSON)
instance AlgorithmI a => ToJSON (SndRatchet a) where
toEncoding = J.genericToEncoding J.defaultOptions
data RcvRatchet = RcvRatchet
{ rcCKr :: RatchetKey,
rcHKr :: HeaderKey
}
deriving (Eq, Show, Generic, FromJSON)
instance ToJSON RcvRatchet where
toEncoding = J.genericToEncoding J.defaultOptions
type SkippedMsgKeys = Map HeaderKey SkippedHdrMsgKeys
type SkippedHdrMsgKeys = Map Word32 MessageKey
data SkippedMsgDiff
= SMDNoChange
| SMDRemove HeaderKey Word32
| SMDAdd SkippedMsgKeys
-- | this function is only used in tests to apply changes in skipped messages,
-- in the agent the diff is persisted, and the whole state is loaded for the next message.
applySMDiff :: SkippedMsgKeys -> SkippedMsgDiff -> SkippedMsgKeys
applySMDiff smks = \case
SMDNoChange -> smks
SMDRemove hk msgN -> fromMaybe smks $ do
mks <- M.lookup hk smks
_ <- M.lookup msgN mks
let mks' = M.delete msgN mks
pure $
if M.null mks'
then M.delete hk smks
else M.insert hk mks' smks
SMDAdd smks' ->
let merge hk mks = M.alter (Just . maybe mks (M.union mks)) hk
in M.foldrWithKey merge smks smks'
type HeaderKey = Key
data MessageKey = MessageKey Key IV
instance Encoding MessageKey where
smpEncode (MessageKey (Key key) (IV iv)) = smpEncode (key, iv)
smpP = MessageKey <$> (Key <$> smpP) <*> (IV <$> smpP)
-- | Input key material for double ratchet HKDF functions
newtype RatchetKey = RatchetKey ByteString
deriving (Eq, Show)
instance ToJSON RatchetKey where
toJSON (RatchetKey k) = strToJSON k
toEncoding (RatchetKey k) = strToJEncoding k
instance FromJSON RatchetKey where
parseJSON = fmap RatchetKey . strParseJSON "Key"
instance AlgorithmI a => ToField (Ratchet a) where toField = toField . LB.toStrict . J.encode
instance (AlgorithmI a, Typeable a) => FromField (Ratchet a) where fromField = blobFieldDecoder J.eitherDecodeStrict'
instance ToField MessageKey where toField = toField . smpEncode
instance FromField MessageKey where fromField = blobFieldDecoder smpDecode
-- | Sending ratchet initialization, equivalent to RatchetInitAliceHE in double ratchet spec
--
-- Please note that sPKey is not stored, and its public part together with random salt
-- is sent to the recipient.
initSndRatchet ::
forall a. (AlgorithmI a, DhAlgorithm a) => PublicKey a -> PrivateKey a -> RatchetInitParams -> Ratchet a
initSndRatchet rcDHRr rcDHRs RatchetInitParams {assocData, ratchetKey, sndHK, rcvNextHK} = do
-- state.RK, state.CKs, state.NHKs = KDF_RK_HE(SK, DH(state.DHRs, state.DHRr))
let (rcRK, rcCKs, rcNHKs) = rootKdf ratchetKey rcDHRr rcDHRs
in Ratchet
{ rcVersion = e2eEncryptVRange,
rcAD = assocData,
rcDHRs,
rcRK,
rcSnd = Just SndRatchet {rcDHRr, rcCKs, rcHKs = sndHK},
rcRcv = Nothing,
rcPN = 0,
rcNs = 0,
rcNr = 0,
rcNHKs,
rcNHKr = rcvNextHK
}
-- | Receiving ratchet initialization, equivalent to RatchetInitBobHE in double ratchet spec
--
-- Please note that the public part of rcDHRs was sent to the sender
-- as part of the connection request and random salt was received from the sender.
initRcvRatchet ::
forall a. (AlgorithmI a, DhAlgorithm a) => PrivateKey a -> RatchetInitParams -> Ratchet a
initRcvRatchet rcDHRs RatchetInitParams {assocData, ratchetKey, sndHK, rcvNextHK} =
Ratchet
{ rcVersion = e2eEncryptVRange,
rcAD = assocData,
rcDHRs,
rcRK = ratchetKey,
rcSnd = Nothing,
rcRcv = Nothing,
rcPN = 0,
rcNs = 0,
rcNr = 0,
rcNHKs = rcvNextHK,
rcNHKr = sndHK
}
data MsgHeader a = MsgHeader
{ -- | max supported ratchet version
msgMaxVersion :: Version,
msgDHRs :: PublicKey a,
msgPN :: Word32,
msgNs :: Word32
}
deriving (Eq, Show)
data AMsgHeader
= forall a.
(AlgorithmI a, DhAlgorithm a) =>
AMsgHeader (SAlgorithm a) (MsgHeader a)
-- to allow extension without increasing the size, the actual header length is:
-- 69 = 2 (original size) + 2 + 1+56 (Curve448) + 4 + 4
paddedHeaderLen :: Int
paddedHeaderLen = 88
-- only used in tests to validate correct padding
-- (2 bytes - version size, 1 byte - header size, not to have it fixed or version-dependent)
fullHeaderLen :: Int
fullHeaderLen = 2 + 1 + paddedHeaderLen + authTagSize + ivSize @AES256
instance AlgorithmI a => Encoding (MsgHeader a) where
smpEncode MsgHeader {msgMaxVersion, msgDHRs, msgPN, msgNs} =
smpEncode (msgMaxVersion, msgDHRs, msgPN, msgNs)
smpP = do
msgMaxVersion <- smpP
msgDHRs <- smpP
msgPN <- smpP
msgNs <- smpP
pure MsgHeader {msgMaxVersion, msgDHRs, msgPN, msgNs}
data EncMessageHeader = EncMessageHeader
{ ehVersion :: Version,
ehIV :: IV,
ehAuthTag :: AuthTag,
ehBody :: ByteString
}
instance Encoding EncMessageHeader where
smpEncode EncMessageHeader {ehVersion, ehIV, ehAuthTag, ehBody} =
smpEncode (ehVersion, ehIV, ehAuthTag, ehBody)
smpP = do
(ehVersion, ehIV, ehAuthTag, ehBody) <- smpP
pure EncMessageHeader {ehVersion, ehIV, ehAuthTag, ehBody}
data EncRatchetMessage = EncRatchetMessage
{ emHeader :: ByteString,
emAuthTag :: AuthTag,
emBody :: ByteString
}
instance Encoding EncRatchetMessage where
smpEncode EncRatchetMessage {emHeader, emBody, emAuthTag} =
smpEncode (emHeader, emAuthTag, Tail emBody)
smpP = do
(emHeader, emAuthTag, Tail emBody) <- smpP
pure EncRatchetMessage {emHeader, emBody, emAuthTag}
rcEncrypt :: AlgorithmI a => Ratchet a -> Int -> ByteString -> ExceptT CryptoError IO (ByteString, Ratchet a)
rcEncrypt Ratchet {rcSnd = Nothing} _ _ = throwE CERatchetState
rcEncrypt rc@Ratchet {rcSnd = Just sr@SndRatchet {rcCKs, rcHKs}, rcDHRs, rcNs, rcPN, rcAD = Str rcAD, rcVersion} paddedMsgLen msg = do
-- state.CKs, mk = KDF_CK(state.CKs)
let (ck', mk, iv, ehIV) = chainKdf rcCKs
-- enc_header = HENCRYPT(state.HKs, header)
(ehAuthTag, ehBody) <- encryptAEAD rcHKs ehIV paddedHeaderLen rcAD msgHeader
-- return enc_header, ENCRYPT(mk, plaintext, CONCAT(AD, enc_header))
let emHeader = smpEncode EncMessageHeader {ehVersion = minVersion rcVersion, ehBody, ehAuthTag, ehIV}
(emAuthTag, emBody) <- encryptAEAD mk iv paddedMsgLen (rcAD <> emHeader) msg
let msg' = smpEncode EncRatchetMessage {emHeader, emBody, emAuthTag}
-- state.Ns += 1
rc' = rc {rcSnd = Just sr {rcCKs = ck'}, rcNs = rcNs + 1}
pure (msg', rc')
where
-- header = HEADER(state.DHRs, state.PN, state.Ns)
msgHeader =
smpEncode
MsgHeader
{ msgMaxVersion = maxVersion rcVersion,
msgDHRs = publicKey rcDHRs,
msgPN = rcPN,
msgNs = rcNs
}
data SkippedMessage a
= SMMessage (DecryptResult a)
| SMHeader (Maybe RatchetStep) (MsgHeader a)
| SMNone
data RatchetStep = AdvanceRatchet | SameRatchet
deriving (Eq)
type DecryptResult a = (Either CryptoError ByteString, Ratchet a, SkippedMsgDiff)
maxSkip :: Word32
maxSkip = 512
rcDecrypt ::
forall a.
(AlgorithmI a, DhAlgorithm a) =>
Ratchet a ->
SkippedMsgKeys ->
ByteString ->
ExceptT CryptoError IO (DecryptResult a)
rcDecrypt rc@Ratchet {rcRcv, rcAD = Str rcAD} rcMKSkipped msg' = do
encMsg@EncRatchetMessage {emHeader} <- parseE CryptoHeaderError smpP msg'
encHdr <- parseE CryptoHeaderError smpP emHeader
-- plaintext = TrySkippedMessageKeysHE(state, enc_header, cipher-text, AD)
decryptSkipped encHdr encMsg >>= \case
SMNone -> do
(rcStep, hdr) <- decryptRcHeader rcRcv encHdr
decryptRcMessage rcStep hdr encMsg
SMHeader rcStep_ hdr ->
case rcStep_ of
Just rcStep -> decryptRcMessage rcStep hdr encMsg
Nothing -> throwE CERatchetHeader
SMMessage r -> pure r
where
decryptRcMessage :: RatchetStep -> MsgHeader a -> EncRatchetMessage -> ExceptT CryptoError IO (DecryptResult a)
decryptRcMessage rcStep MsgHeader {msgDHRs, msgPN, msgNs} encMsg = do
-- if dh_ratchet:
(rc', smks1) <- ratchetStep rcStep
case skipMessageKeys msgNs rc' of
Left e -> pure (Left e, rc', smkDiff smks1)
Right (rc''@Ratchet {rcRcv = Just rr@RcvRatchet {rcCKr}, rcNr}, smks2) -> do
-- state.CKr, mk = KDF_CK(state.CKr)
let (rcCKr', mk, iv, _) = chainKdf rcCKr
-- return DECRYPT (mk, cipher-text, CONCAT (AD, enc_header))
msg <- decryptMessage (MessageKey mk iv) encMsg
-- state . Nr += 1
pure (msg, rc'' {rcRcv = Just rr {rcCKr = rcCKr'}, rcNr = rcNr + 1}, smkDiff $ smks1 <> smks2)
Right (rc'', smks2) -> do
pure (Left CERatchetState, rc'', smkDiff $ smks1 <> smks2)
where
smkDiff :: SkippedMsgKeys -> SkippedMsgDiff
smkDiff smks = if M.null smks then SMDNoChange else SMDAdd smks
ratchetStep :: RatchetStep -> ExceptT CryptoError IO (Ratchet a, SkippedMsgKeys)
ratchetStep SameRatchet = pure (rc, M.empty)
ratchetStep AdvanceRatchet =
-- SkipMessageKeysHE(state, header.pn)
case skipMessageKeys msgPN rc of
Left e -> throwE e
Right (rc'@Ratchet {rcDHRs, rcRK, rcNHKs, rcNHKr}, hmks) -> do
-- DHRatchetHE(state, header)
(_, rcDHRs') <- liftIO $ generateKeyPair' @a
-- state.RK, state.CKr, state.NHKr = KDF_RK_HE(state.RK, DH(state.DHRs, state.DHRr))
let (rcRK', rcCKr', rcNHKr') = rootKdf rcRK msgDHRs rcDHRs
-- state.RK, state.CKs, state.NHKs = KDF_RK_HE(state.RK, DH(state.DHRs, state.DHRr))
(rcRK'', rcCKs', rcNHKs') = rootKdf rcRK' msgDHRs rcDHRs'
rc'' =
rc'
{ rcDHRs = rcDHRs',
rcRK = rcRK'',
rcSnd = Just SndRatchet {rcDHRr = msgDHRs, rcCKs = rcCKs', rcHKs = rcNHKs},
rcRcv = Just RcvRatchet {rcCKr = rcCKr', rcHKr = rcNHKr},
rcPN = rcNs rc,
rcNs = 0,
rcNr = 0,
rcNHKs = rcNHKs',
rcNHKr = rcNHKr'
}
pure (rc'', hmks)
skipMessageKeys :: Word32 -> Ratchet a -> Either CryptoError (Ratchet a, SkippedMsgKeys)
skipMessageKeys _ r@Ratchet {rcRcv = Nothing} = Right (r, M.empty)
skipMessageKeys untilN r@Ratchet {rcRcv = Just rr@RcvRatchet {rcCKr, rcHKr}, rcNr}
| rcNr > untilN = Left CERatchetDuplicateMessage
| rcNr + maxSkip < untilN = Left CERatchetTooManySkipped
| rcNr == untilN = Right (r, M.empty)
| otherwise =
let (rcCKr', rcNr', mks) = advanceRcvRatchet (untilN - rcNr) rcCKr rcNr M.empty
r' = r {rcRcv = Just rr {rcCKr = rcCKr'}, rcNr = rcNr'}
in Right (r', M.singleton rcHKr mks)
advanceRcvRatchet :: Word32 -> RatchetKey -> Word32 -> SkippedHdrMsgKeys -> (RatchetKey, Word32, SkippedHdrMsgKeys)
advanceRcvRatchet 0 ck msgNs mks = (ck, msgNs, mks)
advanceRcvRatchet n ck msgNs mks =
let (ck', mk, iv, _) = chainKdf ck
mks' = M.insert msgNs (MessageKey mk iv) mks
in advanceRcvRatchet (n - 1) ck' (msgNs + 1) mks'
decryptSkipped :: EncMessageHeader -> EncRatchetMessage -> ExceptT CryptoError IO (SkippedMessage a)
decryptSkipped encHdr encMsg = tryDecryptSkipped SMNone $ M.assocs rcMKSkipped
where
tryDecryptSkipped :: SkippedMessage a -> [(HeaderKey, SkippedHdrMsgKeys)] -> ExceptT CryptoError IO (SkippedMessage a)
tryDecryptSkipped SMNone ((hk, mks) : hks) = do
tryE (decryptHeader hk encHdr) >>= \case
Left CERatchetHeader -> tryDecryptSkipped SMNone hks
Left e -> throwE e
Right hdr@MsgHeader {msgNs} ->
case M.lookup msgNs mks of
Nothing ->
let nextRc
| maybe False ((== hk) . rcHKr) rcRcv = Just SameRatchet
| hk == rcNHKr rc = Just AdvanceRatchet
| otherwise = Nothing
in pure $ SMHeader nextRc hdr
Just mk -> do
msg <- decryptMessage mk encMsg
pure $ SMMessage (msg, rc, SMDRemove hk msgNs)
tryDecryptSkipped r _ = pure r
decryptRcHeader :: Maybe RcvRatchet -> EncMessageHeader -> ExceptT CryptoError IO (RatchetStep, MsgHeader a)
decryptRcHeader Nothing hdr = decryptNextHeader hdr
decryptRcHeader (Just RcvRatchet {rcHKr}) hdr =
-- header = HDECRYPT(state.HKr, enc_header)
((SameRatchet,) <$> decryptHeader rcHKr hdr) `catchE` \case
CERatchetHeader -> decryptNextHeader hdr
e -> throwE e
-- header = HDECRYPT(state.NHKr, enc_header)
decryptNextHeader hdr = (AdvanceRatchet,) <$> decryptHeader (rcNHKr rc) hdr
decryptHeader k EncMessageHeader {ehBody, ehAuthTag, ehIV} = do
header <- decryptAEAD k ehIV rcAD ehBody ehAuthTag `catchE` \_ -> throwE CERatchetHeader
parseE' CryptoHeaderError smpP header
decryptMessage :: MessageKey -> EncRatchetMessage -> ExceptT CryptoError IO (Either CryptoError ByteString)
decryptMessage (MessageKey mk iv) EncRatchetMessage {emHeader, emBody, emAuthTag} =
-- DECRYPT(mk, cipher-text, CONCAT(AD, enc_header))
-- TODO add associated data
tryE $ decryptAEAD mk iv (rcAD <> emHeader) emBody emAuthTag
rootKdf :: (AlgorithmI a, DhAlgorithm a) => RatchetKey -> PublicKey a -> PrivateKey a -> (RatchetKey, RatchetKey, Key)
rootKdf (RatchetKey rk) k pk =
let dhOut = dhBytes' $ dh' k pk
(rk', ck, nhk) = hkdf3 rk dhOut "SimpleXRootRatchet"
in (RatchetKey rk', RatchetKey ck, Key nhk)
chainKdf :: RatchetKey -> (RatchetKey, Key, IV, IV)
chainKdf (RatchetKey ck) =
let (ck', mk, ivs) = hkdf3 "" ck "SimpleXChainRatchet"
(iv1, iv2) = B.splitAt 16 ivs
in (RatchetKey ck', Key mk, IV iv1, IV iv2)
hkdf3 :: ByteString -> ByteString -> ByteString -> (ByteString, ByteString, ByteString)
hkdf3 salt ikm info = (s1, s2, s3)
where
prk = H.extract salt ikm :: H.PRK SHA512
out = H.expand prk info 96
(s1, rest) = B.splitAt 32 out
(s2, s3) = B.splitAt 32 rest
+143
View File
@@ -0,0 +1,143 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
module Simplex.Messaging.Encoding
( Encoding (..),
Tail (..),
Large (..),
smpEncodeList,
smpListP,
)
where
import Data.Attoparsec.ByteString.Char8 (Parser)
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.Bits (shiftL, shiftR, (.|.))
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.ByteString.Internal (c2w, w2c)
import Data.Int (Int64)
import qualified Data.List.NonEmpty as L
import Data.Time.Clock.System (SystemTime (..))
import Data.Word (Word16, Word32)
import Network.Transport.Internal (decodeWord16, decodeWord32, encodeWord16, encodeWord32)
import Simplex.Messaging.Parsers (parseAll)
import Simplex.Messaging.Util ((<$?>))
-- | SMP protocol encoding
class Encoding a where
{-# MINIMAL smpEncode, (smpDecode | smpP) #-}
-- | protocol encoding of type (default implementation uses protocol ByteString encoding)
smpEncode :: a -> ByteString
-- | decoding of type (default implementation uses parser)
smpDecode :: ByteString -> Either String a
smpDecode = parseAll smpP
-- | protocol parser of type (default implementation parses protocol ByteString encoding)
smpP :: Parser a
smpP = smpDecode <$?> smpP
instance Encoding Char where
smpEncode = B.singleton
smpP = A.anyChar
instance Encoding Word16 where
smpEncode = encodeWord16
smpP = decodeWord16 <$> A.take 2
instance Encoding Word32 where
smpEncode = encodeWord32
smpP = decodeWord32 <$> A.take 4
instance Encoding Int64 where
smpEncode i = w32 (i `shiftR` 32) <> w32 i
smpP = do
l <- w32P
r <- w32P
pure $ (l `shiftL` 32) .|. r
w32 :: Int64 -> ByteString
w32 = smpEncode @Word32 . fromIntegral
w32P :: Parser Int64
w32P = fromIntegral <$> smpP @Word32
-- ByteStrings are assumed no longer than 255 bytes
instance Encoding ByteString where
smpEncode s = B.cons (lenEncode $ B.length s) s
smpP = A.take =<< lenP
lenEncode :: Int -> Char
lenEncode = w2c . fromIntegral
lenP :: Parser Int
lenP = fromIntegral . c2w <$> A.anyChar
instance Encoding a => Encoding (Maybe a) where
smpEncode s = maybe "0" (("1" <>) . smpEncode) s
smpP =
smpP >>= \case
'0' -> pure Nothing
'1' -> Just <$> smpP
_ -> fail "invalid Maybe tag"
newtype Tail = Tail {unTail :: ByteString}
instance Encoding Tail where
smpEncode = unTail
smpP = Tail <$> A.takeByteString
-- newtype for encoding/decoding ByteStrings over 255 bytes with 2-bytes length prefix
newtype Large = Large {unLarge :: ByteString}
instance Encoding Large where
smpEncode (Large s) = smpEncode @Word16 (fromIntegral $ B.length s) <> s
smpP = do
len <- fromIntegral <$> smpP @Word16
Large <$> A.take len
instance Encoding SystemTime where
smpEncode = smpEncode . systemSeconds
smpP = MkSystemTime <$> smpP <*> pure 0
-- lists encode/parse as a sequence of items prefixed with list length (as 1 byte)
smpEncodeList :: Encoding a => [a] -> ByteString
smpEncodeList xs = B.cons (lenEncode $ length xs) . B.concat $ map smpEncode xs
smpListP :: Encoding a => Parser [a]
smpListP = (`A.count` smpP) =<< lenP
instance Encoding String where
smpEncode = smpEncode . B.pack
smpP = B.unpack <$> smpP
instance Encoding a => Encoding (L.NonEmpty a) where
smpEncode = smpEncodeList . L.toList
smpP =
lenP >>= \case
0 -> fail "empty list"
n -> L.fromList <$> A.count n smpP
instance (Encoding a, Encoding b) => Encoding (a, b) where
smpEncode (a, b) = smpEncode a <> smpEncode b
smpP = (,) <$> smpP <*> smpP
instance (Encoding a, Encoding b, Encoding c) => Encoding (a, b, c) where
smpEncode (a, b, c) = smpEncode a <> smpEncode b <> smpEncode c
smpP = (,,) <$> smpP <*> smpP <*> smpP
instance (Encoding a, Encoding b, Encoding c, Encoding d) => Encoding (a, b, c, d) where
smpEncode (a, b, c, d) = smpEncode a <> smpEncode b <> smpEncode c <> smpEncode d
smpP = (,,,) <$> smpP <*> smpP <*> smpP <*> smpP
instance (Encoding a, Encoding b, Encoding c, Encoding d, Encoding e) => Encoding (a, b, c, d, e) where
smpEncode (a, b, c, d, e) = smpEncode a <> smpEncode b <> smpEncode c <> smpEncode d <> smpEncode e
smpP = (,,,,) <$> smpP <*> smpP <*> smpP <*> smpP <*> smpP
+120
View File
@@ -0,0 +1,120 @@
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
module Simplex.Messaging.Encoding.String
( StrEncoding (..),
Str (..),
strP_,
strToJSON,
strToJEncoding,
strParseJSON,
base64urlP,
strEncodeList,
strListP,
)
where
import Control.Applicative (optional)
import Data.Aeson (FromJSON (..), ToJSON (..))
import qualified Data.Aeson as J
import qualified Data.Aeson.Encoding as JE
import qualified Data.Aeson.Types as JT
import Data.Attoparsec.ByteString.Char8 (Parser)
import qualified Data.Attoparsec.ByteString.Char8 as A
import qualified Data.ByteString.Base64.URL as U
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Char (isAlphaNum)
import qualified Data.List.NonEmpty as L
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
import Data.Word (Word16)
import Simplex.Messaging.Parsers (parseAll)
import Simplex.Messaging.Util ((<$?>))
-- | Serializing human-readable and (where possible) URI-friendly strings for SMP and SMP agent protocols
class StrEncoding a where
{-# MINIMAL strEncode, (strDecode | strP) #-}
strEncode :: a -> ByteString
-- Please note - if you only specify strDecode, it will use base64urlP as default parser before decoding the string
strDecode :: ByteString -> Either String a
strDecode = parseAll strP
strP :: Parser a
strP = strDecode <$?> base64urlP
-- base64url encoding/decoding of ByteStrings - the parser only allows non-empty strings
instance StrEncoding ByteString where
strEncode = U.encode
strDecode = U.decode
strP = base64urlP
base64urlP :: Parser ByteString
base64urlP = do
str <- A.takeWhile1 (\c -> isAlphaNum c || c == '-' || c == '_')
pad <- A.takeWhile (== '=')
either fail pure $ U.decode (str <> pad)
newtype Str = Str {unStr :: ByteString}
deriving (Eq, Show)
instance StrEncoding Str where
strEncode = unStr
strP = Str <$> A.takeTill (== ' ') <* optional A.space
instance ToJSON Str where
toJSON (Str s) = strToJSON s
toEncoding (Str s) = strToJEncoding s
instance FromJSON Str where
parseJSON = fmap Str . strParseJSON "Str"
instance StrEncoding a => StrEncoding (Maybe a) where
strEncode = maybe "" strEncode
strP = optional strP
instance StrEncoding Word16 where
strEncode = B.pack . show
strP = A.decimal
-- lists encode/parse as comma-separated strings
strEncodeList :: StrEncoding a => [a] -> ByteString
strEncodeList = B.intercalate "," . map strEncode
strListP :: StrEncoding a => Parser [a]
strListP = listItem `A.sepBy'` A.char ','
-- relies on sepBy1 never returning an empty list
instance StrEncoding a => StrEncoding (L.NonEmpty a) where
strEncode = strEncodeList . L.toList
strP = L.fromList <$> listItem `A.sepBy1'` A.char ','
listItem :: StrEncoding a => Parser a
listItem = parseAll strP <$?> A.takeTill (== ',')
instance (StrEncoding a, StrEncoding b) => StrEncoding (a, b) where
strEncode (a, b) = B.unwords [strEncode a, strEncode b]
strP = (,) <$> strP_ <*> strP
instance (StrEncoding a, StrEncoding b, StrEncoding c) => StrEncoding (a, b, c) where
strEncode (a, b, c) = B.unwords [strEncode a, strEncode b, strEncode c]
strP = (,,) <$> strP_ <*> strP_ <*> strP
instance (StrEncoding a, StrEncoding b, StrEncoding c, StrEncoding d) => StrEncoding (a, b, c, d) where
strEncode (a, b, c, d) = B.unwords [strEncode a, strEncode b, strEncode c, strEncode d]
strP = (,,,) <$> strP_ <*> strP_ <*> strP_ <*> strP
instance (StrEncoding a, StrEncoding b, StrEncoding c, StrEncoding d, StrEncoding e) => StrEncoding (a, b, c, d, e) where
strEncode (a, b, c, d, e) = B.unwords [strEncode a, strEncode b, strEncode c, strEncode d, strEncode e]
strP = (,,,,) <$> strP_ <*> strP_ <*> strP_ <*> strP_ <*> strP
strP_ :: StrEncoding a => Parser a
strP_ = strP <* A.space
strToJSON :: StrEncoding a => a -> J.Value
strToJSON = J.String . decodeLatin1 . strEncode
strToJEncoding :: StrEncoding a => a -> J.Encoding
strToJEncoding = JE.text . decodeLatin1 . strEncode
strParseJSON :: StrEncoding a => String -> J.Value -> JT.Parser a
strParseJSON name = J.withText name $ either fail pure . parseAll strP . encodeUtf8
+76 -13
View File
@@ -1,15 +1,18 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
module Simplex.Messaging.Parsers where
import Control.Monad.Trans.Except
import qualified Data.Aeson as J
import Data.Attoparsec.ByteString.Char8 (Parser)
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.Bifunctor (first)
import Data.ByteString.Base64
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Char (isAlphaNum)
import Data.Char (isAlphaNum, toLower)
import Data.Time.Clock (UTCTime)
import Data.Time.ISO8601 (parseISO8601)
import Data.Typeable (Typeable)
@@ -21,16 +24,21 @@ import Simplex.Messaging.Util ((<$?>))
import Text.Read (readMaybe)
base64P :: Parser ByteString
base64P = decode <$?> base64StringP
base64P = decode <$?> paddedBase64 rawBase64P
base64StringP :: Parser ByteString
base64StringP = do
str <- A.takeWhile1 (\c -> isAlphaNum c || c == '+' || c == '/')
pad <- A.takeWhile (== '=')
pure $ str <> pad
paddedBase64 :: Parser ByteString -> Parser ByteString
paddedBase64 raw = (<>) <$> raw <*> pad
where
pad = A.takeWhile (== '=')
rawBase64P :: Parser ByteString
rawBase64P = A.takeWhile1 (\c -> isAlphaNum c || c == '+' || c == '/')
-- rawBase64UriP :: Parser ByteString
-- rawBase64UriP = A.takeWhile1 (\c -> isAlphaNum c || c == '-' || c == '_')
tsISO8601P :: Parser UTCTime
tsISO8601P = maybe (fail "timestamp") pure . parseISO8601 . B.unpack =<< A.takeTill (== ' ')
tsISO8601P = maybe (fail "timestamp") pure . parseISO8601 . B.unpack =<< A.takeTill wordEnd
parse :: Parser a -> e -> (ByteString -> Either e a)
parse parser err = first (const err) . parseAll parser
@@ -38,25 +46,80 @@ parse parser err = first (const err) . parseAll parser
parseAll :: Parser a -> (ByteString -> Either String a)
parseAll parser = A.parseOnly (parser <* A.endOfInput)
parseE :: (String -> e) -> Parser a -> (ByteString -> ExceptT e IO a)
parseE err parser = except . first err . parseAll parser
parseE' :: (String -> e) -> Parser a -> (ByteString -> ExceptT e IO a)
parseE' err parser = except . first err . A.parseOnly parser
parseRead :: Read a => Parser ByteString -> Parser a
parseRead = (>>= maybe (fail "cannot read") pure . readMaybe . B.unpack)
parseRead1 :: Read a => Parser a
parseRead1 = parseRead $ A.takeTill (== ' ')
parseRead1 = parseRead $ A.takeTill wordEnd
parseRead2 :: Read a => Parser a
parseRead2 = parseRead $ do
w1 <- A.takeTill (== ' ') <* A.char ' '
w2 <- A.takeTill (== ' ')
w1 <- A.takeTill wordEnd <* A.char ' '
w2 <- A.takeTill wordEnd
pure $ w1 <> " " <> w2
wordEnd :: Char -> Bool
wordEnd c = c == ' ' || c == '\n'
parseString :: (ByteString -> Either String a) -> (String -> a)
parseString p = either error id . p . B.pack
blobFieldParser :: Typeable k => Parser k -> FieldParser k
blobFieldParser p = \case
blobFieldParser = blobFieldDecoder . parseAll
blobFieldDecoder :: Typeable k => (ByteString -> Either String k) -> FieldParser k
blobFieldDecoder dec = \case
f@(Field (SQLBlob b) _) ->
case parseAll p b of
case dec b of
Right k -> Ok k
Left e -> returnError ConversionFailed f ("couldn't parse field: " ++ e)
f -> returnError ConversionFailed f "expecting SQLBlob column type"
fstToLower :: String -> String
fstToLower "" = ""
fstToLower (h : t) = toLower h : t
dropPrefix :: String -> String -> String
dropPrefix pfx s =
let (p, rest) = splitAt (length pfx) s
in fstToLower $ if p == pfx then rest else s
enumJSON :: (String -> String) -> J.Options
enumJSON tagModifier =
J.defaultOptions
{ J.constructorTagModifier = tagModifier,
J.allNullaryToStringTag = True
}
sumTypeJSON :: (String -> String) -> J.Options
#if defined(darwin_HOST_OS)
sumTypeJSON = singleFieldJSON
#else
sumTypeJSON = taggedObjectJSON
#endif
taggedObjectJSON :: (String -> String) -> J.Options
taggedObjectJSON tagModifier =
J.defaultOptions
{ J.sumEncoding = J.TaggedObject "type" "data",
J.constructorTagModifier = tagModifier,
J.allNullaryToStringTag = False,
J.nullaryToObject = True,
J.omitNothingFields = True
}
singleFieldJSON :: (String -> String) -> J.Options
singleFieldJSON tagModifier =
J.defaultOptions
{ J.sumEncoding = J.ObjectWithSingleField,
J.constructorTagModifier = tagModifier,
J.allNullaryToStringTag = False,
J.nullaryToObject = True,
J.omitNothingFields = True
}
+566 -205
View File
@@ -1,11 +1,17 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-}
@@ -23,100 +29,160 @@
--
-- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md
module Simplex.Messaging.Protocol
( -- * SMP protocol types
( -- * SMP protocol parameters
smpClientVersion,
smpClientVRange,
maxMessageLength,
e2eEncConfirmationLength,
e2eEncMessageLength,
-- * SMP protocol types
Protocol,
Command (..),
Party (..),
Cmd (..),
BrokerMsg (..),
SParty (..),
PartyI (..),
QueueIdsKeys (..),
ErrorType (..),
CommandError (..),
Transmission,
SignedTransmission,
SignedTransmissionOrError,
RawTransmission,
SentRawTransmission,
SignedRawTransmission,
ClientMsgEnvelope (..),
PubHeader (..),
ClientMessage (..),
PrivHeader (..),
SMPServer (..),
SrvLoc (..),
CorrId (..),
QueueId,
RecipientId,
SenderId,
RecipientPrivateKey,
RecipientPublicKey,
SenderPrivateKey,
SenderPublicKey,
Encoded,
NotifierId,
RcvPrivateSignKey,
RcvPublicVerifyKey,
RcvPublicDhKey,
RcvDhSecret,
SndPrivateSignKey,
SndPublicVerifyKey,
NtfPrivateSignKey,
NtfPublicVerifyKey,
MsgId,
MsgBody,
-- * Parse and serialize
serializeTransmission,
serializeCommand,
serializeErrorType,
encodeTransmission,
transmissionP,
commandP,
errorTypeP,
encodeProtocol,
-- * TCP transport functions
tPut,
tGet,
fromClient,
fromServer,
-- * exports for tests
CommandTag (..),
BrokerMsgTag (..),
)
where
import Control.Applicative ((<|>))
import Control.Monad
import Control.Applicative (optional, (<|>))
import Control.Monad.Except
import Data.Aeson (ToJSON (..))
import qualified Data.Aeson as J
import Data.Attoparsec.ByteString.Char8 (Parser)
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.ByteString.Base64
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Functor (($>))
import Data.Kind
import Data.Maybe (isNothing)
import Data.String
import Data.Time.Clock
import Data.Time.ISO8601
import Data.Time.Clock.System (SystemTime)
import Data.Type.Equality
import GHC.Generics (Generic)
import Generic.Random (genericArbitraryU)
import Network.Socket (HostName, ServiceName)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers
import Simplex.Messaging.Transport (THandle, Transport, TransportError (..), tGetEncrypted, tPutEncrypted)
import Simplex.Messaging.Util
import Simplex.Messaging.Transport (THandle (..), Transport, TransportError (..), tGetBlock, tPutBlock)
import Simplex.Messaging.Util (bshow, (<$?>))
import Simplex.Messaging.Version
import Test.QuickCheck (Arbitrary (..))
-- | SMP protocol participants.
data Party = Broker | Recipient | Sender
smpClientVersion :: Version
smpClientVersion = 1
smpClientVRange :: VersionRange
smpClientVRange = mkVersionRange 1 smpClientVersion
maxMessageLength :: Int
maxMessageLength = 16088
-- it is shorter to allow per-queue e2e encryption DH key in the "public" header
e2eEncConfirmationLength :: Int
e2eEncConfirmationLength = 15936
e2eEncMessageLength :: Int
e2eEncMessageLength = 16032
-- | SMP protocol clients
data Party = Recipient | Sender | Notifier
deriving (Show)
-- | Singleton types for SMP protocol participants.
-- | Singleton types for SMP protocol clients
data SParty :: Party -> Type where
SBroker :: SParty Broker
SRecipient :: SParty Recipient
SSender :: SParty Sender
SNotifier :: SParty Notifier
deriving instance Show (SParty a)
instance TestEquality SParty where
testEquality SRecipient SRecipient = Just Refl
testEquality SSender SSender = Just Refl
testEquality SNotifier SNotifier = Just Refl
testEquality _ _ = Nothing
-- | Type for command or response of any participant.
data Cmd = forall a. Cmd (SParty a) (Command a)
deriving instance Show (SParty p)
class PartyI (p :: Party) where sParty :: SParty p
instance PartyI Recipient where sParty = SRecipient
instance PartyI Sender where sParty = SSender
instance PartyI Notifier where sParty = SNotifier
-- | Type for client command of any participant.
data Cmd = forall p. PartyI p => Cmd (SParty p) (Command p)
deriving instance Show Cmd
-- | SMP transmission without signature.
type Transmission = (CorrId, QueueId, Cmd)
-- | Parsed SMP transmission without signature, size and session ID.
type Transmission c = (CorrId, QueueId, c)
-- | SMP transmission with signature.
type SignedTransmission = (C.Signature, Transmission)
-- | signed parsed transmission, with original raw bytes and parsing error.
type SignedTransmission c = (Maybe C.ASignature, Signed, Transmission (Either ErrorType c))
type TransmissionOrError = (CorrId, QueueId, Either ErrorType Cmd)
-- | signed parsed transmission, with parsing error.
type SignedTransmissionOrError = (C.Signature, TransmissionOrError)
type Signed = ByteString
-- | unparsed SMP transmission with signature.
type RawTransmission = (ByteString, ByteString, ByteString, ByteString)
data RawTransmission = RawTransmission
{ signature :: ByteString,
signed :: ByteString,
sessId :: ByteString,
corrId :: ByteString,
queueId :: ByteString,
command :: ByteString
}
-- | unparsed SMP transmission with signature.
type SignedRawTransmission = (C.Signature, ByteString)
-- | unparsed sent SMP transmission with signature, without session ID.
type SignedRawTransmission = (Maybe C.ASignature, ByteString, ByteString, ByteString)
-- | unparsed sent SMP transmission with signature.
type SentRawTransmission = (Maybe C.ASignature, ByteString)
-- | SMP queue ID for the recipient.
type RecipientId = QueueId
@@ -124,62 +190,278 @@ type RecipientId = QueueId
-- | SMP queue ID for the sender.
type SenderId = QueueId
-- | SMP queue ID on the server.
type QueueId = Encoded
-- | SMP queue ID for notifications.
type NotifierId = QueueId
-- | Parameterized type for SMP protocol commands from all participants.
data Command (a :: Party) where
-- | SMP queue ID on the server.
type QueueId = ByteString
-- | Parameterized type for SMP protocol commands from all clients.
data Command (p :: Party) where
-- SMP recipient commands
NEW :: RecipientPublicKey -> Command Recipient
NEW :: RcvPublicVerifyKey -> RcvPublicDhKey -> Command Recipient
SUB :: Command Recipient
KEY :: SenderPublicKey -> Command Recipient
KEY :: SndPublicVerifyKey -> Command Recipient
NKEY :: NtfPublicVerifyKey -> Command Recipient
ACK :: Command Recipient
OFF :: Command Recipient
DEL :: Command Recipient
-- SMP sender commands
SEND :: MsgBody -> Command Sender
PING :: Command Sender
-- SMP broker commands (responses, messages, notifications)
IDS :: RecipientId -> SenderId -> Command Broker
MSG :: MsgId -> UTCTime -> MsgBody -> Command Broker
END :: Command Broker
OK :: Command Broker
ERR :: ErrorType -> Command Broker
PONG :: Command Broker
-- SMP notification subscriber commands
NSUB :: Command Notifier
deriving instance Show (Command a)
deriving instance Show (Command p)
deriving instance Eq (Command a)
deriving instance Eq (Command p)
-- | Base-64 encoded string.
type Encoded = ByteString
data BrokerMsg where
-- SMP broker messages (responses, client messages, notifications)
IDS :: QueueIdsKeys -> BrokerMsg
MSG :: MsgId -> SystemTime -> MsgBody -> BrokerMsg
NID :: NotifierId -> BrokerMsg
NMSG :: BrokerMsg
END :: BrokerMsg
OK :: BrokerMsg
ERR :: ErrorType -> BrokerMsg
PONG :: BrokerMsg
deriving (Eq, Show)
-- * SMP command tags
data CommandTag (p :: Party) where
NEW_ :: CommandTag Recipient
SUB_ :: CommandTag Recipient
KEY_ :: CommandTag Recipient
NKEY_ :: CommandTag Recipient
ACK_ :: CommandTag Recipient
OFF_ :: CommandTag Recipient
DEL_ :: CommandTag Recipient
SEND_ :: CommandTag Sender
PING_ :: CommandTag Sender
NSUB_ :: CommandTag Notifier
data CmdTag = forall p. PartyI p => CT (SParty p) (CommandTag p)
deriving instance Show (CommandTag p)
deriving instance Show CmdTag
data BrokerMsgTag
= IDS_
| MSG_
| NID_
| NMSG_
| END_
| OK_
| ERR_
| PONG_
deriving (Show)
class ProtocolMsgTag t where
decodeTag :: ByteString -> Maybe t
messageTagP :: ProtocolMsgTag t => Parser t
messageTagP =
maybe (fail "bad command") pure . decodeTag
=<< (A.takeTill (== ' ') <* optional A.space)
instance PartyI p => Encoding (CommandTag p) where
smpEncode = \case
NEW_ -> "NEW"
SUB_ -> "SUB"
KEY_ -> "KEY"
NKEY_ -> "NKEY"
ACK_ -> "ACK"
OFF_ -> "OFF"
DEL_ -> "DEL"
SEND_ -> "SEND"
PING_ -> "PING"
NSUB_ -> "NSUB"
smpP = messageTagP
instance ProtocolMsgTag CmdTag where
decodeTag = \case
"NEW" -> Just $ CT SRecipient NEW_
"SUB" -> Just $ CT SRecipient SUB_
"KEY" -> Just $ CT SRecipient KEY_
"NKEY" -> Just $ CT SRecipient NKEY_
"ACK" -> Just $ CT SRecipient ACK_
"OFF" -> Just $ CT SRecipient OFF_
"DEL" -> Just $ CT SRecipient DEL_
"SEND" -> Just $ CT SSender SEND_
"PING" -> Just $ CT SSender PING_
"NSUB" -> Just $ CT SNotifier NSUB_
_ -> Nothing
instance Encoding CmdTag where
smpEncode (CT _ t) = smpEncode t
smpP = messageTagP
instance PartyI p => ProtocolMsgTag (CommandTag p) where
decodeTag s = decodeTag s >>= (\(CT _ t) -> checkParty' t)
instance Encoding BrokerMsgTag where
smpEncode = \case
IDS_ -> "IDS"
MSG_ -> "MSG"
NID_ -> "NID"
NMSG_ -> "NMSG"
END_ -> "END"
OK_ -> "OK"
ERR_ -> "ERR"
PONG_ -> "PONG"
smpP = messageTagP
instance ProtocolMsgTag BrokerMsgTag where
decodeTag = \case
"IDS" -> Just IDS_
"MSG" -> Just MSG_
"NID" -> Just NID_
"NMSG" -> Just NMSG_
"END" -> Just END_
"OK" -> Just OK_
"ERR" -> Just ERR_
"PONG" -> Just PONG_
_ -> Nothing
-- | SMP message body format
data ClientMsgEnvelope = ClientMsgEnvelope
{ cmHeader :: PubHeader,
cmNonce :: C.CbNonce,
cmEncBody :: ByteString
}
deriving (Show)
data PubHeader = PubHeader
{ phVersion :: Version,
phE2ePubDhKey :: Maybe C.PublicKeyX25519
}
deriving (Show)
instance Encoding PubHeader where
smpEncode (PubHeader v k) = smpEncode (v, k)
smpP = PubHeader <$> smpP <*> smpP
instance Encoding ClientMsgEnvelope where
smpEncode ClientMsgEnvelope {cmHeader, cmNonce, cmEncBody} =
smpEncode (cmHeader, cmNonce, Tail cmEncBody)
smpP = do
(cmHeader, cmNonce, Tail cmEncBody) <- smpP
pure ClientMsgEnvelope {cmHeader, cmNonce, cmEncBody}
data ClientMessage = ClientMessage PrivHeader ByteString
data PrivHeader
= PHConfirmation C.APublicVerifyKey
| PHEmpty
deriving (Show)
instance Encoding PrivHeader where
smpEncode = \case
PHConfirmation k -> "K" <> smpEncode k
PHEmpty -> "_"
smpP =
A.anyChar >>= \case
'K' -> PHConfirmation <$> smpP
'_' -> pure PHEmpty
_ -> fail "invalid PrivHeader"
instance Encoding ClientMessage where
smpEncode (ClientMessage h msg) = smpEncode h <> msg
smpP = ClientMessage <$> smpP <*> A.takeByteString
-- | SMP server location and transport key digest (hash).
data SMPServer = SMPServer
{ host :: HostName,
port :: ServiceName,
keyHash :: C.KeyHash
}
deriving (Eq, Ord, Show)
instance IsString SMPServer where
fromString = parseString strDecode
instance Encoding SMPServer where
smpEncode SMPServer {host, port, keyHash} =
smpEncode (host, port, keyHash)
smpP = do
(host, port, keyHash) <- smpP
pure SMPServer {host, port, keyHash}
instance StrEncoding SMPServer where
strEncode SMPServer {host, port, keyHash} =
"smp://" <> strEncode keyHash <> "@" <> strEncode (SrvLoc host port)
strP = do
_ <- "smp://"
keyHash <- strP <* A.char '@'
SrvLoc host port <- strP
pure SMPServer {host, port, keyHash}
data SrvLoc = SrvLoc HostName ServiceName
deriving (Eq, Ord, Show)
instance StrEncoding SrvLoc where
strEncode (SrvLoc host port) = B.pack $ host <> if null port then "" else ':' : port
strP = SrvLoc <$> host <*> (port <|> pure "")
where
host = B.unpack <$> A.takeWhile1 (A.notInClass ":#,;/ ")
port = B.unpack <$> (A.char ':' *> A.takeWhile1 A.isDigit)
-- | Transmission correlation ID.
--
-- A newtype to avoid accidentally changing order of transmission parts.
newtype CorrId = CorrId {bs :: ByteString} deriving (Eq, Ord, Show)
instance IsString CorrId where
fromString = CorrId . fromString
instance StrEncoding CorrId where
strEncode (CorrId cId) = strEncode cId
strDecode s = CorrId <$> strDecode s
strP = CorrId <$> strP
instance ToJSON CorrId where
toJSON = strToJSON
toEncoding = strToJEncoding
-- | Queue IDs and keys
data QueueIdsKeys = QIK
{ rcvId :: RecipientId,
sndId :: SenderId,
rcvPublicDhKey :: RcvPublicDhKey
}
deriving (Eq, Show)
-- | Recipient's private key used by the recipient to authorize (sign) SMP commands.
--
-- Only used by SMP agent, kept here so its definition is close to respective public key.
type RecipientPrivateKey = C.SafePrivateKey
type RcvPrivateSignKey = C.APrivateSignKey
-- | Recipient's public key used by SMP server to verify authorization of SMP commands.
type RecipientPublicKey = C.PublicKey
type RcvPublicVerifyKey = C.APublicVerifyKey
-- | Public key used for DH exchange to encrypt message bodies from server to recipient
type RcvPublicDhKey = C.PublicKeyX25519
-- | DH Secret used to encrypt message bodies from server to recipient
type RcvDhSecret = C.DhSecretX25519
-- | Sender's private key used by the recipient to authorize (sign) SMP commands.
--
-- Only used by SMP agent, kept here so its definition is close to respective public key.
type SenderPrivateKey = C.SafePrivateKey
type SndPrivateSignKey = C.APrivateSignKey
-- | Sender's public key used by SMP server to verify authorization of SMP commands.
type SenderPublicKey = C.PublicKey
type SndPublicVerifyKey = C.APublicVerifyKey
-- | Private key used by push notifications server to authorize (sign) LSTN command.
type NtfPrivateSignKey = C.APrivateSignKey
-- | Public key used by SMP server to verify authorization of LSTN command sent by push notifications server.
type NtfPublicVerifyKey = C.APublicVerifyKey
-- | SMP message server ID.
type MsgId = Encoded
type MsgId = ByteString
-- | SMP message body.
type MsgBody = ByteString
@@ -188,24 +470,38 @@ type MsgBody = ByteString
data ErrorType
= -- | incorrect block format, encoding or signature size
BLOCK
| -- | incorrect SMP session ID (TLS Finished message / tls-unique binding RFC5929)
SESSION
| -- | SMP command is unknown or has invalid syntax
CMD CommandError
CMD {cmdErr :: CommandError}
| -- | command authorization error - bad signature or non-existing SMP queue
AUTH
| -- | SMP queue capacity is exceeded on the server
QUOTA
| -- | ACK command is sent without message to be acknowledged
NO_MSG
| -- | sent message is too large (> maxMessageLength = 16078 bytes)
LARGE_MSG
| -- | internal server error
INTERNAL
| -- | used internally, never returned by the server (to be removed)
DUPLICATE_ -- TODO remove, not part of SMP protocol
deriving (Eq, Generic, Read, Show)
instance ToJSON ErrorType where
toJSON = J.genericToJSON $ sumTypeJSON id
toEncoding = J.genericToEncoding $ sumTypeJSON id
instance StrEncoding ErrorType where
strEncode = \case
CMD e -> "CMD " <> bshow e
e -> bshow e
strP = "CMD " *> (CMD <$> parseRead1) <|> parseRead1
-- | SMP command error type.
data CommandError
= -- | server response sent from client or vice versa
PROHIBITED
| -- | bad RSA key size in NEW or KEY commands (only 1024, 2048 and 4096 bits keys are allowed)
KEY_SIZE
= -- | unknown command
UNKNOWN
| -- | error parsing command
SYNTAX
| -- | transmission has no required credentials (signature or queue ID)
@@ -216,6 +512,10 @@ data CommandError
NO_QUEUE
deriving (Eq, Generic, Read, Show)
instance ToJSON CommandError where
toJSON = J.genericToJSON $ sumTypeJSON id
toEncoding = J.genericToEncoding $ sumTypeJSON id
instance Arbitrary ErrorType where arbitrary = genericArbitraryU
instance Arbitrary CommandError where arbitrary = genericArbitraryU
@@ -223,156 +523,217 @@ instance Arbitrary CommandError where arbitrary = genericArbitraryU
-- | SMP transmission parser.
transmissionP :: Parser RawTransmission
transmissionP = do
signature <- segment
corrId <- segment
queueId <- segment
command <- A.takeByteString
return (signature, corrId, queueId, command)
signature <- smpP
signed <- A.takeByteString
either fail pure $ parseAll (trn signature signed) signed
where
segment = A.takeTill (== ' ') <* " "
trn signature signed = do
sessId <- smpP
corrId <- smpP
queueId <- smpP
command <- A.takeByteString
pure RawTransmission {signature, signed, sessId, corrId, queueId, command}
-- | SMP command parser.
commandP :: Parser Cmd
commandP =
"NEW " *> newCmd
<|> "IDS " *> idsResp
<|> "SUB" $> Cmd SRecipient SUB
<|> "KEY " *> keyCmd
<|> "ACK" $> Cmd SRecipient ACK
<|> "OFF" $> Cmd SRecipient OFF
<|> "DEL" $> Cmd SRecipient DEL
<|> "SEND " *> sendCmd
<|> "PING" $> Cmd SSender PING
<|> "MSG " *> message
<|> "END" $> Cmd SBroker END
<|> "OK" $> Cmd SBroker OK
<|> "ERR " *> serverError
<|> "PONG" $> Cmd SBroker PONG
where
newCmd = Cmd SRecipient . NEW <$> C.pubKeyP
idsResp = Cmd SBroker <$> (IDS <$> (base64P <* A.space) <*> base64P)
keyCmd = Cmd SRecipient . KEY <$> C.pubKeyP
sendCmd = do
size <- A.decimal <* A.space
Cmd SSender . SEND <$> A.take size <* A.space
message = do
msgId <- base64P <* A.space
ts <- tsISO8601P <* A.space
size <- A.decimal <* A.space
Cmd SBroker . MSG msgId ts <$> A.take size <* A.space
serverError = Cmd SBroker . ERR <$> errorTypeP
class Protocol msg where
type Tag msg
encodeProtocol :: msg -> ByteString
protocolP :: Tag msg -> Parser msg
checkCredentials :: SignedRawTransmission -> msg -> Either ErrorType msg
-- TODO ignore the end of block, no need to parse it
instance PartyI p => Protocol (Command p) where
type Tag (Command p) = CommandTag p
encodeProtocol = \case
NEW rKey dhKey -> e (NEW_, ' ', rKey, dhKey)
SUB -> e SUB_
KEY k -> e (KEY_, ' ', k)
NKEY k -> e (NKEY_, ' ', k)
ACK -> e ACK_
OFF -> e OFF_
DEL -> e DEL_
SEND msg -> e (SEND_, ' ', Tail msg)
PING -> e PING_
NSUB -> e NSUB_
where
e :: Encoding a => a -> ByteString
e = smpEncode
-- | Parse SMP command.
parseCommand :: ByteString -> Either ErrorType Cmd
parseCommand = parse (commandP <* " " <* A.takeByteString) $ CMD SYNTAX
protocolP tag = (\(Cmd _ c) -> checkParty c) <$?> protocolP (CT (sParty @p) tag)
-- | Serialize SMP command.
serializeCommand :: Cmd -> ByteString
serializeCommand = \case
Cmd SRecipient (NEW rKey) -> "NEW " <> C.serializePubKey rKey
Cmd SRecipient (KEY sKey) -> "KEY " <> C.serializePubKey sKey
Cmd SRecipient cmd -> bshow cmd
Cmd SSender (SEND msgBody) -> "SEND " <> serializeMsg msgBody
Cmd SSender PING -> "PING"
Cmd SBroker (MSG msgId ts msgBody) ->
B.unwords ["MSG", encode msgId, B.pack $ formatISO8601Millis ts, serializeMsg msgBody]
Cmd SBroker (IDS rId sId) -> B.unwords ["IDS", encode rId, encode sId]
Cmd SBroker (ERR err) -> "ERR " <> serializeErrorType err
Cmd SBroker resp -> bshow resp
where
serializeMsg msgBody = bshow (B.length msgBody) <> " " <> msgBody <> " "
checkCredentials (sig, _, queueId, _) cmd = case cmd of
-- NEW must have signature but NOT queue ID
NEW {}
| isNothing sig -> Left $ CMD NO_AUTH
| not (B.null queueId) -> Left $ CMD HAS_AUTH
| otherwise -> Right cmd
-- SEND must have queue ID, signature is not always required
SEND _
| B.null queueId -> Left $ CMD NO_QUEUE
| otherwise -> Right cmd
-- PING must not have queue ID or signature
PING
| isNothing sig && B.null queueId -> Right cmd
| otherwise -> Left $ CMD HAS_AUTH
-- other client commands must have both signature and queue ID
_
| isNothing sig || B.null queueId -> Left $ CMD NO_AUTH
| otherwise -> Right cmd
-- | SMP error parser.
errorTypeP :: Parser ErrorType
errorTypeP = "CMD " *> (CMD <$> parseRead1) <|> parseRead1
instance Protocol Cmd where
type Tag Cmd = CmdTag
encodeProtocol (Cmd _ c) = encodeProtocol c
-- | Serialize SMP error.
serializeErrorType :: ErrorType -> ByteString
serializeErrorType = bshow
protocolP = \case
CT SRecipient tag ->
Cmd SRecipient <$> case tag of
NEW_ -> NEW <$> _smpP <*> smpP
SUB_ -> pure SUB
KEY_ -> KEY <$> _smpP
NKEY_ -> NKEY <$> _smpP
ACK_ -> pure ACK
OFF_ -> pure OFF
DEL_ -> pure DEL
CT SSender tag ->
Cmd SSender <$> case tag of
SEND_ -> SEND . unTail <$> _smpP
PING_ -> pure PING
CT SNotifier NSUB_ -> pure $ Cmd SNotifier NSUB
checkCredentials t (Cmd p c) = Cmd p <$> checkCredentials t c
instance Protocol BrokerMsg where
type Tag BrokerMsg = BrokerMsgTag
encodeProtocol = \case
IDS (QIK rcvId sndId srvDh) -> e (IDS_, ' ', rcvId, sndId, srvDh)
MSG msgId ts msgBody -> e (MSG_, ' ', msgId, ts, Tail msgBody)
NID nId -> e (NID_, ' ', nId)
NMSG -> e NMSG_
END -> e END_
OK -> e OK_
ERR err -> e (ERR_, ' ', err)
PONG -> e PONG_
where
e :: Encoding a => a -> ByteString
e = smpEncode
protocolP = \case
MSG_ -> MSG <$> _smpP <*> smpP <*> (unTail <$> smpP)
IDS_ -> IDS <$> (QIK <$> _smpP <*> smpP <*> smpP)
NID_ -> NID <$> _smpP
NMSG_ -> pure NMSG
END_ -> pure END
OK_ -> pure OK
ERR_ -> ERR <$> _smpP
PONG_ -> pure PONG
checkCredentials (_, _, queueId, _) cmd = case cmd of
-- IDS response must not have queue ID
IDS _ -> Right cmd
-- ERR response does not always have queue ID
ERR _ -> Right cmd
-- PONG response must not have queue ID
PONG
| B.null queueId -> Right cmd
| otherwise -> Left $ CMD HAS_AUTH
-- other broker responses must have queue ID
_
| B.null queueId -> Left $ CMD NO_QUEUE
| otherwise -> Right cmd
_smpP :: Encoding a => Parser a
_smpP = A.space *> smpP
-- | Parse SMP protocol commands and broker messages
parseProtocol :: (Protocol msg, ProtocolMsgTag (Tag msg)) => ByteString -> Either ErrorType msg
parseProtocol s =
let (tag, params) = B.break (== ' ') s
in case decodeTag tag of
Just cmd -> parse (protocolP cmd) (CMD SYNTAX) params
Nothing -> Left $ CMD UNKNOWN
checkParty :: forall t p p'. (PartyI p, PartyI p') => t p' -> Either String (t p)
checkParty c = case testEquality (sParty @p) (sParty @p') of
Just Refl -> Right c
Nothing -> Left "bad command party"
checkParty' :: forall t p p'. (PartyI p, PartyI p') => t p' -> Maybe (t p)
checkParty' c = case testEquality (sParty @p) (sParty @p') of
Just Refl -> Just c
_ -> Nothing
instance Encoding ErrorType where
smpEncode = \case
BLOCK -> "BLOCK"
SESSION -> "SESSION"
CMD err -> "CMD " <> smpEncode err
AUTH -> "AUTH"
QUOTA -> "QUOTA"
NO_MSG -> "NO_MSG"
LARGE_MSG -> "LARGE_MSG"
INTERNAL -> "INTERNAL"
DUPLICATE_ -> "DUPLICATE_"
smpP =
A.takeTill (== ' ') >>= \case
"BLOCK" -> pure BLOCK
"SESSION" -> pure SESSION
"CMD" -> CMD <$> _smpP
"AUTH" -> pure AUTH
"QUOTA" -> pure QUOTA
"NO_MSG" -> pure NO_MSG
"LARGE_MSG" -> pure LARGE_MSG
"INTERNAL" -> pure INTERNAL
"DUPLICATE_" -> pure DUPLICATE_
_ -> fail "bad error type"
instance Encoding CommandError where
smpEncode e = case e of
UNKNOWN -> "UNKNOWN"
SYNTAX -> "SYNTAX"
NO_AUTH -> "NO_AUTH"
HAS_AUTH -> "HAS_AUTH"
NO_QUEUE -> "NO_QUEUE"
smpP =
A.takeTill (== ' ') >>= \case
"UNKNOWN" -> pure UNKNOWN
"SYNTAX" -> pure SYNTAX
"NO_AUTH" -> pure NO_AUTH
"HAS_AUTH" -> pure HAS_AUTH
"NO_QUEUE" -> pure NO_QUEUE
_ -> fail "bad command error type"
-- | Send signed SMP transmission to TCP transport.
tPut :: Transport c => THandle c -> SignedRawTransmission -> IO (Either TransportError ())
tPut th (C.Signature sig, t) =
tPutEncrypted th $ encode sig <> " " <> t <> " "
tPut :: Transport c => THandle c -> SentRawTransmission -> IO (Either TransportError ())
tPut th (sig, t) = tPutBlock th $ smpEncode (C.signatureBytes sig) <> t
-- | Serialize SMP transmission.
serializeTransmission :: Transmission -> ByteString
serializeTransmission (CorrId corrId, queueId, command) =
B.intercalate " " [corrId, encode queueId, serializeCommand command]
encodeTransmission :: Protocol c => ByteString -> Transmission c -> ByteString
encodeTransmission sessionId (CorrId corrId, queueId, command) =
smpEncode (sessionId, corrId, queueId) <> encodeProtocol command
-- | Validate that it is an SMP client command, used with 'tGet' by 'Simplex.Messaging.Server'.
fromClient :: Cmd -> Either ErrorType Cmd
fromClient = \case
Cmd SBroker _ -> Left $ CMD PROHIBITED
cmd -> Right cmd
-- | Validate that it is an SMP server command, used with 'tGet' by 'Simplex.Messaging.Client'.
fromServer :: Cmd -> Either ErrorType Cmd
fromServer = \case
cmd@(Cmd SBroker _) -> Right cmd
_ -> Left $ CMD PROHIBITED
-- | Receive and parse transmission from the TCP transport.
-- | Receive and parse transmission from the TCP transport (ignoring any trailing padding).
tGetParse :: Transport c => THandle c -> IO (Either TransportError RawTransmission)
tGetParse th = (>>= parse transmissionP TEBadBlock) <$> tGetEncrypted th
tGetParse th = (parse transmissionP TEBadBlock =<<) <$> tGetBlock th
-- | Receive client and server transmissions.
--
-- The first argument is used to limit allowed senders.
-- 'fromClient' or 'fromServer' should be used here.
tGet :: forall c m. (Transport c, MonadIO m) => (Cmd -> Either ErrorType Cmd) -> THandle c -> m SignedTransmissionOrError
tGet fromParty th = liftIO (tGetParse th) >>= decodeParseValidate
-- | Receive client and server transmissions (determined by `cmd` type).
tGet ::
forall cmd c m.
(Protocol cmd, ProtocolMsgTag (Tag cmd), Transport c, MonadIO m) =>
THandle c ->
m (SignedTransmission cmd)
tGet th@THandle {sessionId} = liftIO (tGetParse th) >>= decodeParseValidate
where
decodeParseValidate :: Either TransportError RawTransmission -> m SignedTransmissionOrError
decodeParseValidate :: Either TransportError RawTransmission -> m (SignedTransmission cmd)
decodeParseValidate = \case
Right (signature, corrId, queueId, command) ->
let decodedTransmission = liftM2 (,corrId,,command) (validSig =<< decode signature) (decode queueId)
in either (const $ tError corrId) tParseValidate decodedTransmission
Right RawTransmission {signature, signed, sessId, corrId, queueId, command}
| sessId == sessionId ->
let decodedTransmission = (,corrId,queueId,command) <$> C.decodeSignature signature
in either (const $ tError corrId) (tParseValidate signed) decodedTransmission
| otherwise -> pure (Nothing, "", (CorrId corrId, "", Left SESSION))
Left _ -> tError ""
validSig :: ByteString -> Either String ByteString
validSig sig
| B.null sig || C.validKeySize (B.length sig) = Right sig
| otherwise = Left "invalid signature size"
tError :: ByteString -> m (SignedTransmission cmd)
tError corrId = pure (Nothing, "", (CorrId corrId, "", Left BLOCK))
tError :: ByteString -> m SignedTransmissionOrError
tError corrId = return (C.Signature "", (CorrId corrId, "", Left BLOCK))
tParseValidate :: RawTransmission -> m SignedTransmissionOrError
tParseValidate t@(sig, corrId, queueId, command) = do
let cmd = parseCommand command >>= fromParty >>= tCredentials t
return (C.Signature sig, (CorrId corrId, queueId, cmd))
tCredentials :: RawTransmission -> Cmd -> Either ErrorType Cmd
tCredentials (signature, _, queueId, _) cmd = case cmd of
-- IDS response must not have queue ID
Cmd SBroker (IDS _ _) -> Right cmd
-- ERR response does not always have queue ID
Cmd SBroker (ERR _) -> Right cmd
-- PONG response must not have queue ID
Cmd SBroker PONG
| B.null queueId -> Right cmd
| otherwise -> Left $ CMD HAS_AUTH
-- other responses must have queue ID
Cmd SBroker _
| B.null queueId -> Left $ CMD NO_QUEUE
| otherwise -> Right cmd
-- NEW must have signature but NOT queue ID
Cmd SRecipient (NEW _)
| B.null signature -> Left $ CMD NO_AUTH
| not (B.null queueId) -> Left $ CMD HAS_AUTH
| otherwise -> Right cmd
-- SEND must have queue ID, signature is not always required
Cmd SSender (SEND _)
| B.null queueId -> Left $ CMD NO_QUEUE
| otherwise -> Right cmd
-- PING must not have queue ID or signature
Cmd SSender PING
| B.null queueId && B.null signature -> Right cmd
| otherwise -> Left $ CMD HAS_AUTH
-- other client commands must have both signature and queue ID
Cmd SRecipient _
| B.null signature || B.null queueId -> Left $ CMD NO_AUTH
| otherwise -> Right cmd
tParseValidate :: ByteString -> SignedRawTransmission -> m (SignedTransmission cmd)
tParseValidate signed t@(sig, corrId, queueId, command) = do
let cmd = parseProtocol command >>= checkCredentials t
pure (sig, signed, (CorrId corrId, queueId, cmd))
+229 -149
View File
@@ -35,7 +35,9 @@ import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Functor (($>))
import qualified Data.Map.Strict as M
import Data.Time.Clock
import Data.Maybe (isNothing)
import Data.Time.Clock.System (getSystemTime)
import Data.Type.Equality
import Network.Socket (ServiceName)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Protocol
@@ -46,6 +48,7 @@ import Simplex.Messaging.Server.QueueStore
import Simplex.Messaging.Server.QueueStore.STM (QueueStore)
import Simplex.Messaging.Server.StoreLog
import Simplex.Messaging.Transport
import Simplex.Messaging.Transport.Server
import Simplex.Messaging.Util
import UnliftIO.Concurrent
import UnliftIO.Exception
@@ -72,39 +75,79 @@ runSMPServerBlocking started cfg@ServerConfig {transports} = do
smpServer :: (MonadUnliftIO m', MonadReader Env m') => m' ()
smpServer = do
s <- asks server
raceAny_ (serverThread s : map runServer transports)
raceAny_
( serverThread s subscribedQ subscribers subscriptions cancelSub :
serverThread s ntfSubscribedQ notifiers ntfSubscriptions (\_ -> pure ()) :
map runServer transports
)
`finally` withLog closeStoreLog
runServer :: (MonadUnliftIO m', MonadReader Env m') => (ServiceName, ATransport) -> m' ()
runServer (tcpPort, ATransport t) = runTransportServer started tcpPort (runClient t)
runServer (tcpPort, ATransport t) = do
serverParams <- asks tlsServerParams
runTransportServer started tcpPort serverParams (runClient t)
serverThread :: MonadUnliftIO m' => Server -> m' ()
serverThread Server {subscribedQ, subscribers} = forever . atomically $ do
(rId, clnt) <- readTBQueue subscribedQ
cs <- readTVar subscribers
case M.lookup rId cs of
Just Client {rcvQ} -> writeTBQueue rcvQ (CorrId B.empty, rId, Cmd SBroker END)
Nothing -> return ()
writeTVar subscribers $ M.insert rId clnt cs
serverThread ::
forall m' s.
MonadUnliftIO m' =>
Server ->
(Server -> TBQueue (QueueId, Client)) ->
(Server -> TVar (M.Map QueueId Client)) ->
(Client -> TVar (M.Map QueueId s)) ->
(s -> m' ()) ->
m' ()
serverThread s subQ subs clientSubs unsub = forever $ do
atomically updateSubscribers
>>= fmap join . mapM endPreviousSubscriptions
>>= mapM_ unsub
where
updateSubscribers :: STM (Maybe (QueueId, Client))
updateSubscribers = do
(qId, clnt) <- readTBQueue $ subQ s
let clientToBeNotified = \c' ->
if sameClientSession clnt c'
then pure Nothing
else do
yes <- readTVar $ connected c'
pure $ if yes then Just (qId, c') else Nothing
stateTVar (subs s) (\cs -> (M.lookup qId cs, M.insert qId clnt cs))
>>= fmap join . mapM clientToBeNotified
endPreviousSubscriptions :: (QueueId, Client) -> m' (Maybe s)
endPreviousSubscriptions (qId, c) = do
void . forkIO . atomically $
writeTBQueue (sndQ c) (CorrId "", qId, END)
atomically . stateTVar (clientSubs c) $ \ss -> (M.lookup qId ss, M.delete qId ss)
runClient :: (Transport c, MonadUnliftIO m, MonadReader Env m) => TProxy c -> c -> m ()
runClient _ h = do
keyPair <- asks serverKeyPair
liftIO (runExceptT $ serverHandshake h keyPair) >>= \case
Right th -> runClientTransport th
Left _ -> pure ()
runClient :: (Transport c, MonadUnliftIO m, MonadReader Env m) => TProxy c -> c -> m ()
runClient _ h = do
kh <- asks serverIdentity
liftIO (runExceptT $ serverHandshake h kh) >>= \case
Right th -> runClientTransport th
Left _ -> pure ()
runClientTransport :: (Transport c, MonadUnliftIO m, MonadReader Env m) => THandle c -> m ()
runClientTransport th = do
runClientTransport th@THandle {sessionId} = do
q <- asks $ tbqSize . config
c <- atomically $ newClient q
c <- atomically $ newClient q sessionId
s <- asks server
raceAny_ [send th c, client c s, receive th c]
`finally` cancelSubscribers c
`finally` clientDisconnected c
cancelSubscribers :: MonadUnliftIO m => Client -> m ()
cancelSubscribers Client {subscriptions} =
readTVarIO subscriptions >>= mapM_ cancelSub
clientDisconnected :: (MonadUnliftIO m, MonadReader Env m) => Client -> m ()
clientDisconnected c@Client {subscriptions, connected} = do
atomically $ writeTVar connected False
subs <- readTVarIO subscriptions
mapM_ cancelSub subs
cs <- asks $ subscribers . server
atomically . mapM_ (modifyTVar cs . M.update deleteCurrentClient) $ M.keys subs
where
deleteCurrentClient :: Client -> Maybe Client
deleteCurrentClient c'
| sameClientSession c c' = Nothing
| otherwise = Just c'
sameClientSession :: Client -> Client -> Bool
sameClientSession Client {sessionId} Client {sessionId = s'} = sessionId == s'
cancelSub :: MonadUnliftIO m => Sub -> m ()
cancelSub = \case
@@ -112,109 +155,118 @@ cancelSub = \case
_ -> return ()
receive :: (Transport c, MonadUnliftIO m, MonadReader Env m) => THandle c -> Client -> m ()
receive h Client {rcvQ} = forever $ do
(signature, (corrId, queueId, cmdOrError)) <- tGet fromClient h
t <- case cmdOrError of
Left e -> return . mkResp corrId queueId $ ERR e
Right cmd -> verifyTransmission (signature, (corrId, queueId, cmd))
atomically $ writeTBQueue rcvQ t
receive th Client {rcvQ, sndQ} = forever $ do
(sig, signed, (corrId, queueId, cmdOrError)) <- tGet th
case cmdOrError of
Left e -> write sndQ (corrId, queueId, ERR e)
Right cmd -> do
verified <- verifyTransmission sig signed queueId cmd
if verified
then write rcvQ (corrId, queueId, cmd)
else write sndQ (corrId, queueId, ERR AUTH)
where
write q t = atomically $ writeTBQueue q t
send :: (Transport c, MonadUnliftIO m) => THandle c -> Client -> m ()
send h Client {sndQ} = forever $ do
send h Client {sndQ, sessionId} = forever $ do
t <- atomically $ readTBQueue sndQ
liftIO $ tPut h ("", serializeTransmission t)
liftIO $ tPut h (Nothing, encodeTransmission sessionId t)
mkResp :: CorrId -> QueueId -> Command 'Broker -> Transmission
mkResp corrId queueId command = (corrId, queueId, Cmd SBroker command)
verifyTransmission :: forall m. (MonadUnliftIO m, MonadReader Env m) => SignedTransmission -> m Transmission
verifyTransmission (sig, t@(corrId, queueId, cmd)) = do
(corrId,queueId,) <$> case cmd of
Cmd SBroker _ -> return $ smpErr INTERNAL -- it can only be client command, because `fromClient` was used
Cmd SRecipient (NEW k) -> pure $ verifySignature k
verifyTransmission ::
forall m. (MonadUnliftIO m, MonadReader Env m) => Maybe C.ASignature -> ByteString -> QueueId -> Cmd -> m Bool
verifyTransmission sig_ signed queueId cmd = do
case cmd of
Cmd SRecipient (NEW k _) -> pure $ verifySignature k
Cmd SRecipient _ -> verifyCmd SRecipient $ verifySignature . recipientKey
Cmd SSender (SEND _) -> verifyCmd SSender $ verifySend sig . senderKey
Cmd SSender PING -> return cmd
Cmd SSender (SEND _) -> verifyCmd SSender $ verifyMaybe . senderKey
Cmd SSender PING -> pure True
Cmd SNotifier NSUB -> verifyCmd SNotifier $ verifyMaybe . fmap snd . notifier
where
verifyCmd :: SParty p -> (QueueRec -> Cmd) -> m Cmd
verifyCmd :: SParty p -> (QueueRec -> Bool) -> m Bool
verifyCmd party f = do
st <- asks queueStore
q <- atomically $ getQueue st party queueId
pure $ either (const $ dummyVerify authErr) f q
verifySend :: C.Signature -> Maybe SenderPublicKey -> Cmd
verifySend "" = maybe cmd (const authErr)
verifySend _ = maybe authErr verifySignature
verifySignature :: C.PublicKey -> Cmd
verifySignature key = if verify key then cmd else authErr
verify key
| C.publicKeySize key == sigLen = cryptoVerify key
| otherwise = dummyVerify False
cryptoVerify key = C.verify key sig (serializeTransmission t)
smpErr = Cmd SBroker . ERR
authErr = smpErr AUTH
dummyVerify :: a -> a
dummyVerify = seq $
cryptoVerify $ case sigLen of
128 -> dummyKey128
256 -> dummyKey256
512 -> dummyKey512
_ -> dummyKey256
sigLen = B.length $ C.unSignature sig
pure $ either (const $ maybe False dummyVerify sig_ `seq` False) f q
verifyMaybe :: Maybe C.APublicVerifyKey -> Bool
verifyMaybe = maybe (isNothing sig_) verifySignature
verifySignature :: C.APublicVerifyKey -> Bool
verifySignature key = maybe False (verify key) sig_
verify :: C.APublicVerifyKey -> C.ASignature -> Bool
verify (C.APublicVerifyKey a k) sig@(C.ASignature a' s) =
case (testEquality a a', C.signatureSize k == C.signatureSize s) of
(Just Refl, True) -> C.verify' k s signed
_ -> dummyVerify sig `seq` False
dummyVerify :: C.ASignature -> Bool
dummyVerify (C.ASignature _ s) = C.verify' (dummyPublicKey s) s signed
-- These dummy keys are used with `dummyVerify` function to mitigate timing attacks
-- by having the same time of the response whether a queue exists or nor, for all valid key/signature sizes
dummyKey128 :: C.PublicKey
dummyKey128 = "MIIBIDANBgkqhkiG9w0BAQEFAAOCAQ0AMIIBCAKBgQC2oeA7s4roXN5K2N6022I1/2CTeMKjWH0m00bSZWa4N8LDKeFcShh8YUxZea5giAveViTRNOOVLgcuXbKvR3u24szN04xP0+KnYUuUUIIoT3YSjX0IlomhDhhSyup4BmA0gAZ+D1OaIKZFX6J8yQ1Lr/JGLEfSRsBjw8l+4hs9OwKBgQDKA+YlZvGb3BcpDwKmatiCXN7ZRDWkjXbj8VAW5zV95tSRCCVN48hrFM1H4Ju2QMMUc6kPUVX+eW4ZjdCl5blIqIHMcTmsdcmsDDCg3PjUNrwc6bv/1TcirbAKcmnKt9iurIt6eerxSO7TZUXXMUVsi7eRwb/RUNhpCrpJ/hpIOw=="
dummyPublicKey :: C.Signature a -> C.PublicKey a
dummyPublicKey = \case
C.SignatureEd25519 _ -> dummyKeyEd25519
C.SignatureEd448 _ -> dummyKeyEd448
dummyKey256 :: C.PublicKey
dummyKey256 = "MIIBoDANBgkqhkiG9w0BAQEFAAOCAY0AMIIBiAKCAQEAxwmTvaqmdTbkfUGNi8Yu0L/T4cxuOlQlx3zGZ9X9Qx0+oZjknWK+QHrdWTcpS+zH4Hi7fP6kanOQoQ90Hj6Ghl57VU1GEdUPywSw4i1/7t0Wv9uT9Q2ktHp2rqVo3xkC9IVIpL7EZAxdRviIN2OsOB3g4a/F1ZpjxcAaZeOMUugiAX1+GtkLuE0Xn4neYjCaOghLxQTdhybN70VtnkiQLx/X9NjkDIl/spYGm3tQFMyYKkP6IWoEpj0926hJ0fmlmhy8tAOhlZsb/baW5cgkEZ3E9jVVrySCgQzoLQgma610FIISRpRJbSyv26jU7MkMxiyuBiDaFOORkXFttoKbtQKBgEbDS9II2brsz+vfI7uP8atFcawkE52cx4M1UWQhqb1H3tBiRl+qO+dMq1pPQF2bW7dlZAWYzS4W/367bTAuALHBDGB8xi1P4Njhh9vaOgTvuqrHG9NJQ85BLy0qGw8rjIWSIXVmVpfrXFJ8po5l04UE258Ll2yocv3QRQmddQW9"
dummyKeyEd25519 :: C.PublicKey 'C.Ed25519
dummyKeyEd25519 = "MCowBQYDK2VwAyEA139Oqs4QgpqbAmB0o7rZf6T19ryl7E65k4AYe0kE3Qs="
dummyKey512 :: C.PublicKey
dummyKey512 = "MIICoDANBgkqhkiG9w0BAQEFAAOCAo0AMIICiAKCAgEArkCY9DuverJ4mmzDektv9aZMFyeRV46WZK9NsOBKEc+1ncqMs+LhLti9asKNgUBRbNzmbOe0NYYftrUpwnATaenggkTFxxbJ4JGJuGYbsEdFWkXSvrbWGtM8YUmn5RkAGme12xQ89bSM4VoJAGnrYPHwmcQd+KYCPZvTUsxaxgrJTX65ejHN9BsAn8XtGViOtHTDJO9yUMD2WrJvd7wnNa+0ugEteDLzMU++xS98VC+uA1vfauUqi3yXVchdfrLdVUuM+JE0gUEXCgzjuHkaoHiaGNiGhdPYoAJJdOKQOIHAKdk7Th6OPhirPhc9XYNB4O8JDthKhNtfokvFIFlC4QBRzJhpLIENaEBDt08WmgpOnecZB/CuxkqqOrNa8j5K5jNrtXAI67W46VEC2jeQy/gZwb64Zit2A4D00xXzGbQTPGj4ehcEMhLx5LSCygViEf0w0tN3c3TEyUcgPzvECd2ZVpQLr9Z4a07Ebr+YSuxcHhjg4Rg1VyJyOTTvaCBGm5X2B3+tI4NUttmikIHOYpBnsLmHY2BgfH2KcrIsDyAhInXmTFr/L2+erFarUnlfATd2L8Ti43TNHDedO6k6jI5Gyi62yPwjqPLEIIK8l+pIeNfHJ3pPmjhHBfzFcQLMMMXffHWNK8kWklrQXK+4j4HiPcTBvlO1FEtG9nEIZhUCgYA4a6WtI2k5YNli1C89GY5rGUY7RP71T6RWri/D3Lz9T7GvU+FemAyYmsvCQwqijUOur0uLvwSP8VdxpSUcrjJJSWur2hrPWzWlu0XbNaeizxpFeKbQP+zSrWJ1z8RwfAeUjShxt8q1TuqGqY10wQyp3nyiTGvS+KwZVj5h5qx8NQ=="
dummyKeyEd448 :: C.PublicKey 'C.Ed448
dummyKeyEd448 = "MEMwBQYDK2VxAzoA6ibQc9XpkSLtwrf7PLvp81qW/etiumckVFImCMRdftcG/XopbOSaq9qyLhrgJWKOLyNrQPNVvpMA"
client :: forall m. (MonadUnliftIO m, MonadReader Env m) => Client -> Server -> m ()
client clnt@Client {subscriptions, rcvQ, sndQ} Server {subscribedQ} =
client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ} Server {subscribedQ, ntfSubscribedQ, notifiers} =
forever $
atomically (readTBQueue rcvQ)
>>= processCommand
>>= atomically . writeTBQueue sndQ
where
processCommand :: Transmission -> m Transmission
processCommand :: Transmission Cmd -> m (Transmission BrokerMsg)
processCommand (corrId, queueId, cmd) = do
st <- asks queueStore
case cmd of
Cmd SBroker END -> unsubscribeQueue $> (corrId, queueId, cmd)
Cmd SBroker _ -> return (corrId, queueId, cmd)
Cmd SSender command -> case command of
SEND msgBody -> sendMessage st msgBody
PING -> return (corrId, queueId, Cmd SBroker PONG)
Cmd SRecipient command -> case command of
NEW rKey -> createQueue st rKey
SUB -> subscribeQueue queueId
ACK -> acknowledgeMsg
KEY sKey -> secureQueue_ st sKey
OFF -> suspendQueue_ st
DEL -> delQueueAndMsgs st
Cmd SSender command ->
case command of
SEND msgBody -> sendMessage st msgBody
PING -> pure (corrId, "", PONG)
Cmd SNotifier NSUB -> subscribeNotifications
Cmd SRecipient command ->
case command of
NEW rKey dhKey -> createQueue st rKey dhKey
SUB -> subscribeQueue queueId
ACK -> acknowledgeMsg
KEY sKey -> secureQueue_ st sKey
NKEY nKey -> addQueueNotifier_ st nKey
OFF -> suspendQueue_ st
DEL -> delQueueAndMsgs st
where
createQueue :: QueueStore -> RecipientPublicKey -> m Transmission
createQueue st rKey =
checkKeySize rKey addSubscribe
createQueue :: QueueStore -> RcvPublicVerifyKey -> RcvPublicDhKey -> m (Transmission BrokerMsg)
createQueue st recipientKey dhKey = do
(rcvPublicDhKey, privDhKey) <- liftIO C.generateKeyPair'
let rcvDhSecret = C.dh' dhKey privDhKey
qik (rcvId, sndId) = QIK {rcvId, sndId, rcvPublicDhKey}
qRec (recipientId, senderId) =
QueueRec
{ recipientId,
senderId,
recipientKey,
rcvDhSecret,
senderKey = Nothing,
notifier = Nothing,
status = QueueActive
}
(corrId,queueId,) <$> addQueueRetry 3 qik qRec
where
addSubscribe =
addQueueRetry 3 >>= \case
Left e -> return $ ERR e
Right (rId, sId) -> do
addQueueRetry ::
Int -> ((RecipientId, SenderId) -> QueueIdsKeys) -> ((RecipientId, SenderId) -> QueueRec) -> m BrokerMsg
addQueueRetry 0 _ _ = pure $ ERR INTERNAL
addQueueRetry n qik qRec = do
ids@(rId, _) <- getIds
-- create QueueRec record with these ids and keys
atomically (addQueue st $ qRec ids) >>= \case
Left DUPLICATE_ -> addQueueRetry (n - 1) qik qRec
Left e -> pure $ ERR e
Right _ -> do
withLog (`logCreateById` rId)
subscribeQueue rId $> IDS rId sId
addQueueRetry :: Int -> m (Either ErrorType (RecipientId, SenderId))
addQueueRetry 0 = return $ Left INTERNAL
addQueueRetry n = do
ids <- getIds
atomically (addQueue st rKey ids) >>= \case
Left DUPLICATE_ -> addQueueRetry $ n - 1
Left e -> return $ Left e
Right _ -> return $ Right ids
subscribeQueue rId $> IDS (qik ids)
logCreateById :: StoreLog 'WriteMode -> RecipientId -> IO ()
logCreateById s rId =
@@ -227,24 +279,31 @@ client clnt@Client {subscriptions, rcvQ, sndQ} Server {subscribedQ} =
n <- asks $ queueIdBytes . config
liftM2 (,) (randomId n) (randomId n)
secureQueue_ :: QueueStore -> SenderPublicKey -> m Transmission
secureQueue_ :: QueueStore -> SndPublicVerifyKey -> m (Transmission BrokerMsg)
secureQueue_ st sKey = do
withLog $ \s -> logSecureQueue s queueId sKey
atomically . checkKeySize sKey $ either ERR (const OK) <$> secureQueue st queueId sKey
atomically $ (corrId,queueId,) . either ERR (const OK) <$> secureQueue st queueId sKey
checkKeySize :: Monad m' => C.PublicKey -> m' (Command 'Broker) -> m' Transmission
checkKeySize key action =
mkResp corrId queueId
<$> if C.validKeySize $ C.publicKeySize key
then action
else pure . ERR $ CMD KEY_SIZE
addQueueNotifier_ :: QueueStore -> NtfPublicVerifyKey -> m (Transmission BrokerMsg)
addQueueNotifier_ st nKey = (corrId,queueId,) <$> addNotifierRetry 3
where
addNotifierRetry :: Int -> m BrokerMsg
addNotifierRetry 0 = pure $ ERR INTERNAL
addNotifierRetry n = do
nId <- randomId =<< asks (queueIdBytes . config)
atomically (addQueueNotifier st queueId nId nKey) >>= \case
Left DUPLICATE_ -> addNotifierRetry $ n - 1
Left e -> pure $ ERR e
Right _ -> do
withLog $ \s -> logAddNotifier s queueId nId nKey
pure $ NID nId
suspendQueue_ :: QueueStore -> m Transmission
suspendQueue_ :: QueueStore -> m (Transmission BrokerMsg)
suspendQueue_ st = do
withLog (`logDeleteQueue` queueId)
okResp <$> atomically (suspendQueue st queueId)
subscribeQueue :: RecipientId -> m Transmission
subscribeQueue :: RecipientId -> m (Transmission BrokerMsg)
subscribeQueue rId =
atomically (getSubscription rId) >>= deliverMessage tryPeekMsg rId
@@ -259,13 +318,15 @@ client clnt@Client {subscriptions, rcvQ, sndQ} Server {subscribedQ} =
writeTVar subscriptions $ M.insert rId s subs
return s
unsubscribeQueue :: m ()
unsubscribeQueue = do
sub <- atomically . stateTVar subscriptions $
\cs -> (M.lookup queueId cs, M.delete queueId cs)
mapM_ cancelSub sub
subscribeNotifications :: m (Transmission BrokerMsg)
subscribeNotifications = atomically $ do
subs <- readTVar ntfSubscriptions
when (isNothing $ M.lookup queueId subs) $ do
writeTBQueue ntfSubscribedQ (queueId, clnt)
writeTVar ntfSubscriptions $ M.insert queueId () subs
pure ok
acknowledgeMsg :: m Transmission
acknowledgeMsg :: m (Transmission BrokerMsg)
acknowledgeMsg =
atomically (withSub queueId $ \s -> const s <$$> tryTakeTMVar (delivered s))
>>= \case
@@ -275,37 +336,56 @@ client clnt@Client {subscriptions, rcvQ, sndQ} Server {subscribedQ} =
withSub :: RecipientId -> (Sub -> STM a) -> STM (Maybe a)
withSub rId f = readTVar subscriptions >>= mapM f . M.lookup rId
sendMessage :: QueueStore -> MsgBody -> m Transmission
sendMessage st msgBody = do
qr <- atomically $ getQueue st SSender queueId
either (return . err) storeMessage qr
sendMessage :: QueueStore -> MsgBody -> m (Transmission BrokerMsg)
sendMessage st msgBody
| B.length msgBody > maxMessageLength = pure $ err LARGE_MSG
| otherwise = do
qr <- atomically $ getQueue st SSender queueId
either (return . err) storeMessage qr
where
mkMessage :: m Message
mkMessage = do
msgId <- asks (msgIdBytes . config) >>= randomId
ts <- liftIO getCurrentTime
return $ Message {msgId, ts, msgBody}
storeMessage :: QueueRec -> m Transmission
storeMessage :: QueueRec -> m (Transmission BrokerMsg)
storeMessage qr = case status qr of
QueueOff -> return $ err AUTH
QueueActive -> do
ms <- asks msgStore
msg <- mkMessage
atomically $ do
q <- getMsgQueue ms (recipientId qr)
writeMsg q msg
return ok
QueueActive ->
mkMessage >>= \case
Left _ -> pure $ err LARGE_MSG
Right msg -> do
ms <- asks msgStore
quota <- asks $ msgQueueQuota . config
atomically $ do
q <- getMsgQueue ms (recipientId qr) quota
ifM (isFull q) (pure $ err QUOTA) $ do
trySendNotification
writeMsg q msg
pure ok
where
mkMessage :: m (Either C.CryptoError Message)
mkMessage = do
msgId <- randomId =<< asks (msgIdBytes . config)
ts <- liftIO getSystemTime
let c = C.cbEncrypt (rcvDhSecret qr) (C.cbNonce msgId) msgBody (maxMessageLength + 2)
pure $ Message msgId ts <$> c
deliverMessage :: (MsgQueue -> STM (Maybe Message)) -> RecipientId -> Sub -> m Transmission
trySendNotification :: STM ()
trySendNotification =
forM_ (notifier qr) $ \(nId, _) ->
mapM_ (writeNtf nId) . M.lookup nId =<< readTVar notifiers
writeNtf :: NotifierId -> Client -> STM ()
writeNtf nId Client {sndQ = q} =
unlessM (isFullTBQueue sndQ) $
writeTBQueue q (CorrId "", nId, NMSG)
deliverMessage :: (MsgQueue -> STM (Maybe Message)) -> RecipientId -> Sub -> m (Transmission BrokerMsg)
deliverMessage tryPeek rId = \case
Sub {subThread = NoSub} -> do
ms <- asks msgStore
q <- atomically $ getMsgQueue ms rId
quota <- asks $ msgQueueQuota . config
q <- atomically $ getMsgQueue ms rId quota
atomically (tryPeek q) >>= \case
Nothing -> forkSub q $> ok
Just msg -> atomically setDelivered $> mkResp corrId rId (msgCmd msg)
_ -> return ok
Just msg -> atomically setDelivered $> (corrId, rId, msgCmd msg)
_ -> pure ok
where
forkSub :: MsgQueue -> m ()
forkSub q = do
@@ -318,7 +398,7 @@ client clnt@Client {subscriptions, rcvQ, sndQ} Server {subscribedQ} =
subscriber :: MsgQueue -> m ()
subscriber q = atomically $ do
msg <- peekMsg q
writeTBQueue sndQ $ mkResp (CorrId B.empty) rId (msgCmd msg)
writeTBQueue sndQ (CorrId "", rId, msgCmd msg)
setSub (\s -> s {subThread = NoSub})
void setDelivered
@@ -328,33 +408,33 @@ client clnt@Client {subscriptions, rcvQ, sndQ} Server {subscribedQ} =
setDelivered :: STM (Maybe Bool)
setDelivered = withSub rId $ \s -> tryPutTMVar (delivered s) ()
delQueueAndMsgs :: QueueStore -> m Transmission
msgCmd :: Message -> BrokerMsg
msgCmd Message {msgId, ts, msgBody} = MSG msgId ts msgBody
delQueueAndMsgs :: QueueStore -> m (Transmission BrokerMsg)
delQueueAndMsgs st = do
withLog (`logDeleteQueue` queueId)
ms <- asks msgStore
atomically $
deleteQueue st queueId >>= \case
Left e -> return $ err e
Left e -> pure $ err e
Right _ -> delMsgQueue ms queueId $> ok
ok :: Transmission
ok = mkResp corrId queueId OK
ok :: Transmission BrokerMsg
ok = (corrId, queueId, OK)
err :: ErrorType -> Transmission
err = mkResp corrId queueId . ERR
err :: ErrorType -> Transmission BrokerMsg
err e = (corrId, queueId, ERR e)
okResp :: Either ErrorType () -> Transmission
okResp :: Either ErrorType () -> Transmission BrokerMsg
okResp = either err $ const ok
msgCmd :: Message -> Command 'Broker
msgCmd Message {msgId, ts, msgBody} = MSG msgId ts msgBody
withLog :: (MonadUnliftIO m, MonadReader Env m) => (StoreLog 'WriteMode -> IO a) -> m ()
withLog action = do
env <- ask
liftIO . mapM_ action $ storeLog (env :: Env)
randomId :: (MonadUnliftIO m, MonadReader Env m) => Int -> m Encoded
randomId :: (MonadUnliftIO m, MonadReader Env m) => Int -> m ByteString
randomId n = do
gVar <- asks idsDrg
atomically (randomBytes n gVar)
+47 -18
View File
@@ -8,49 +8,63 @@ module Simplex.Messaging.Server.Env.STM where
import Control.Concurrent (ThreadId)
import Control.Monad.IO.Unlift
import Crypto.Random
import Data.ByteString.Char8 (ByteString)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Data.X509.Validation (Fingerprint (..))
import Network.Socket (ServiceName)
import qualified Network.TLS as T
import Numeric.Natural
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto (KeyHash (..))
import Simplex.Messaging.Protocol
import Simplex.Messaging.Server.MsgStore.STM
import Simplex.Messaging.Server.QueueStore (QueueRec (..))
import Simplex.Messaging.Server.QueueStore.STM
import Simplex.Messaging.Server.StoreLog
import Simplex.Messaging.Transport (ATransport)
import Simplex.Messaging.Transport.Server (loadFingerprint, loadTLSServerParams)
import System.IO (IOMode (..))
import UnliftIO.STM
data ServerConfig = ServerConfig
{ transports :: [(ServiceName, ATransport)],
tbqSize :: Natural,
serverTbqSize :: Natural,
msgQueueQuota :: Natural,
queueIdBytes :: Int,
msgIdBytes :: Int,
storeLog :: Maybe (StoreLog 'ReadMode),
serverPrivateKey :: C.FullPrivateKey
-- serverId :: ByteString
-- CA certificate private key is not needed for initialization
caCertificateFile :: FilePath,
privateKeyFile :: FilePath,
certificateFile :: FilePath
}
data Env = Env
{ config :: ServerConfig,
server :: Server,
serverIdentity :: KeyHash,
queueStore :: QueueStore,
msgStore :: STMMsgStore,
idsDrg :: TVar ChaChaDRG,
serverKeyPair :: C.FullKeyPair,
storeLog :: Maybe (StoreLog 'WriteMode)
storeLog :: Maybe (StoreLog 'WriteMode),
tlsServerParams :: T.ServerParams
}
data Server = Server
{ subscribedQ :: TBQueue (RecipientId, Client),
subscribers :: TVar (Map RecipientId Client)
subscribers :: TVar (Map RecipientId Client),
ntfSubscribedQ :: TBQueue (NotifierId, Client),
notifiers :: TVar (Map NotifierId Client)
}
data Client = Client
{ subscriptions :: TVar (Map RecipientId Sub),
rcvQ :: TBQueue Transmission,
sndQ :: TBQueue Transmission
ntfSubscriptions :: TVar (Map NotifierId ()),
rcvQ :: TBQueue (Transmission Cmd),
sndQ :: TBQueue (Transmission BrokerMsg),
sessionId :: ByteString,
connected :: TVar Bool
}
data SubscriptionThread = NoSub | SubPending | SubThread ThreadId
@@ -64,14 +78,18 @@ newServer :: Natural -> STM Server
newServer qSize = do
subscribedQ <- newTBQueue qSize
subscribers <- newTVar M.empty
return Server {subscribedQ, subscribers}
ntfSubscribedQ <- newTBQueue qSize
notifiers <- newTVar M.empty
return Server {subscribedQ, subscribers, ntfSubscribedQ, notifiers}
newClient :: Natural -> STM Client
newClient qSize = do
newClient :: Natural -> ByteString -> STM Client
newClient qSize sessionId = do
subscriptions <- newTVar M.empty
ntfSubscriptions <- newTVar M.empty
rcvQ <- newTBQueue qSize
sndQ <- newTBQueue qSize
return Client {subscriptions, rcvQ, sndQ}
connected <- newTVar True
return Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId, connected}
newSubscription :: STM Sub
newSubscription = do
@@ -79,20 +97,31 @@ newSubscription = do
return Sub {subThread = NoSub, delivered}
newEnv :: forall m. (MonadUnliftIO m, MonadRandom m) => ServerConfig -> m Env
newEnv config = do
server <- atomically $ newServer (tbqSize config)
newEnv config@ServerConfig {caCertificateFile, certificateFile, privateKeyFile} = do
server <- atomically $ newServer (serverTbqSize config)
queueStore <- atomically newQueueStore
msgStore <- atomically newMsgStore
idsDrg <- drgNew >>= newTVarIO
s' <- restoreQueues queueStore `mapM` storeLog (config :: ServerConfig)
let pk = serverPrivateKey config
serverKeyPair = (C.publicKey pk, pk)
return Env {config, server, queueStore, msgStore, idsDrg, serverKeyPair, storeLog = s'}
tlsServerParams <- liftIO $ loadTLSServerParams caCertificateFile certificateFile privateKeyFile
Fingerprint fp <- liftIO $ loadFingerprint caCertificateFile
let serverIdentity = KeyHash fp
return Env {config, server, serverIdentity, queueStore, msgStore, idsDrg, storeLog = s', tlsServerParams}
where
restoreQueues :: QueueStore -> StoreLog 'ReadMode -> m (StoreLog 'WriteMode)
restoreQueues queueStore s = do
(queues, s') <- liftIO $ readWriteStoreLog s
atomically $ modifyTVar queueStore $ \d -> d {queues, senders = M.foldr' addSender M.empty queues}
atomically $
modifyTVar queueStore $ \d ->
d
{ queues,
senders = M.foldr' addSender M.empty queues,
notifiers = M.foldr' addNotifier M.empty queues
}
pure s'
addSender :: QueueRec -> Map SenderId RecipientId -> Map SenderId RecipientId
addSender q = M.insert (senderId q) (recipientId q)
addNotifier :: QueueRec -> Map NotifierId RecipientId -> Map NotifierId RecipientId
addNotifier q = case notifier q of
Nothing -> id
Just (nId, _) -> M.insert nId (recipientId q)
+7 -5
View File
@@ -2,20 +2,22 @@
module Simplex.Messaging.Server.MsgStore where
import Data.Time.Clock
import Simplex.Messaging.Protocol (Encoded, MsgBody, RecipientId)
import Data.Time.Clock.System (SystemTime)
import Numeric.Natural
import Simplex.Messaging.Protocol (MsgBody, MsgId, RecipientId)
data Message = Message
{ msgId :: Encoded,
ts :: UTCTime,
{ msgId :: MsgId,
ts :: SystemTime,
msgBody :: MsgBody
}
class MonadMsgStore s q m | s -> q where
getMsgQueue :: s -> RecipientId -> m q
getMsgQueue :: s -> RecipientId -> Natural -> m q
delMsgQueue :: s -> RecipientId -> m ()
class MonadMsgQueue q m where
isFull :: q -> m Bool
writeMsg :: q -> Message -> m () -- non blocking
tryPeekMsg :: q -> m (Maybe Message) -- non blocking
peekMsg :: q -> m Message -- blocking
+12 -8
View File
@@ -8,11 +8,12 @@ module Simplex.Messaging.Server.MsgStore.STM where
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Numeric.Natural
import Simplex.Messaging.Protocol (RecipientId)
import Simplex.Messaging.Server.MsgStore
import UnliftIO.STM
newtype MsgQueue = MsgQueue {msgQueue :: TQueue Message}
newtype MsgQueue = MsgQueue {msgQueue :: TBQueue Message}
newtype MsgStoreData = MsgStoreData {messages :: Map RecipientId MsgQueue}
@@ -22,13 +23,13 @@ newMsgStore :: STM STMMsgStore
newMsgStore = newTVar $ MsgStoreData M.empty
instance MonadMsgStore STMMsgStore MsgQueue STM where
getMsgQueue :: STMMsgStore -> RecipientId -> STM MsgQueue
getMsgQueue store rId = do
getMsgQueue :: STMMsgStore -> RecipientId -> Natural -> STM MsgQueue
getMsgQueue store rId quota = do
m <- messages <$> readTVar store
maybe (newQ m) return $ M.lookup rId m
where
newQ m' = do
q <- MsgQueue <$> newTQueue
q <- MsgQueue <$> newTBQueue quota
writeTVar store . MsgStoreData $ M.insert rId q m'
return q
@@ -37,15 +38,18 @@ instance MonadMsgStore STMMsgStore MsgQueue STM where
modifyTVar store $ MsgStoreData . M.delete rId . messages
instance MonadMsgQueue MsgQueue STM where
isFull :: MsgQueue -> STM Bool
isFull = isFullTBQueue . msgQueue
writeMsg :: MsgQueue -> Message -> STM ()
writeMsg = writeTQueue . msgQueue
writeMsg = writeTBQueue . msgQueue
tryPeekMsg :: MsgQueue -> STM (Maybe Message)
tryPeekMsg = tryPeekTQueue . msgQueue
tryPeekMsg = tryPeekTBQueue . msgQueue
peekMsg :: MsgQueue -> STM Message
peekMsg = peekTQueue . msgQueue
peekMsg = peekTBQueue . msgQueue
-- atomic delete (== read) last and peek next message if available
tryDelPeekMsg :: MsgQueue -> STM (Maybe Message)
tryDelPeekMsg (MsgQueue q) = tryReadTQueue q >> tryPeekTQueue q
tryDelPeekMsg (MsgQueue q) = tryReadTBQueue q >> tryPeekTBQueue q
+11 -19
View File
@@ -1,35 +1,27 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-}
module Simplex.Messaging.Server.QueueStore where
import Simplex.Messaging.Protocol
data QueueRec = QueueRec
{ recipientId :: QueueId,
senderId :: QueueId,
recipientKey :: RecipientPublicKey,
senderKey :: Maybe SenderPublicKey,
{ recipientId :: RecipientId,
recipientKey :: RcvPublicVerifyKey,
rcvDhSecret :: RcvDhSecret,
senderId :: SenderId,
senderKey :: Maybe SndPublicVerifyKey,
notifier :: Maybe (NotifierId, NtfPublicVerifyKey),
status :: QueueStatus
}
data QueueStatus = QueueActive | QueueOff deriving (Eq)
data QueueStatus = QueueActive | QueueOff deriving (Eq, Show)
class MonadQueueStore s m where
addQueue :: s -> RecipientPublicKey -> (RecipientId, SenderId) -> m (Either ErrorType ())
getQueue :: s -> SParty (a :: Party) -> QueueId -> m (Either ErrorType QueueRec)
secureQueue :: s -> RecipientId -> SenderPublicKey -> m (Either ErrorType ())
addQueue :: s -> QueueRec -> m (Either ErrorType ())
getQueue :: s -> SParty p -> QueueId -> m (Either ErrorType QueueRec)
secureQueue :: s -> RecipientId -> SndPublicVerifyKey -> m (Either ErrorType QueueRec)
addQueueNotifier :: s -> RecipientId -> NotifierId -> NtfPublicVerifyKey -> m (Either ErrorType QueueRec)
suspendQueue :: s -> RecipientId -> m (Either ErrorType ())
deleteQueue :: s -> RecipientId -> m (Either ErrorType ())
mkQueueRec :: RecipientPublicKey -> (RecipientId, SenderId) -> QueueRec
mkQueueRec recipientKey (recipientId, senderId) =
QueueRec
{ recipientId,
senderId,
recipientKey,
senderKey = Nothing,
status = QueueActive
}
+43 -18
View File
@@ -19,44 +19,69 @@ import UnliftIO.STM
data QueueStoreData = QueueStoreData
{ queues :: Map RecipientId QueueRec,
senders :: Map SenderId RecipientId
senders :: Map SenderId RecipientId,
notifiers :: Map NotifierId RecipientId
}
type QueueStore = TVar QueueStoreData
newQueueStore :: STM QueueStore
newQueueStore = newTVar QueueStoreData {queues = M.empty, senders = M.empty}
newQueueStore = newTVar QueueStoreData {queues = M.empty, senders = M.empty, notifiers = M.empty}
instance MonadQueueStore QueueStore STM where
addQueue :: QueueStore -> RecipientPublicKey -> (RecipientId, SenderId) -> STM (Either ErrorType ())
addQueue store rKey ids@(rId, sId) = do
addQueue :: QueueStore -> QueueRec -> STM (Either ErrorType ())
addQueue store qRec@QueueRec {recipientId = rId, senderId = sId} = do
cs@QueueStoreData {queues, senders} <- readTVar store
if M.member rId queues || M.member sId senders
then return $ Left DUPLICATE_
else do
writeTVar store $
cs
{ queues = M.insert rId (mkQueueRec rKey ids) queues,
{ queues = M.insert rId qRec queues,
senders = M.insert sId rId senders
}
return $ Right ()
getQueue :: QueueStore -> SParty (p :: Party) -> QueueId -> STM (Either ErrorType QueueRec)
getQueue store SRecipient rId = do
cs <- readTVar store
return $ getRcpQueue cs rId
getQueue store SSender sId = do
cs <- readTVar store
let rId = M.lookup sId $ senders cs
return $ maybe (Left AUTH) (getRcpQueue cs) rId
getQueue _ SBroker _ =
return $ Left INTERNAL
getQueue :: QueueStore -> SParty p -> QueueId -> STM (Either ErrorType QueueRec)
getQueue st party qId = do
cs <- readTVar st
pure $ case party of
SRecipient -> getRcpQueue cs qId
SSender -> getPartyQueue cs senders
SNotifier -> getPartyQueue cs notifiers
where
getPartyQueue ::
QueueStoreData ->
(QueueStoreData -> Map QueueId RecipientId) ->
Either ErrorType QueueRec
getPartyQueue cs recipientIds =
case M.lookup qId $ recipientIds cs of
Just rId -> getRcpQueue cs rId
Nothing -> Left AUTH
secureQueue :: QueueStore -> RecipientId -> SndPublicVerifyKey -> STM (Either ErrorType QueueRec)
secureQueue store rId sKey =
updateQueues store rId $ \cs c ->
case senderKey c of
Just _ -> (Left AUTH, cs)
_ -> (Right (), cs {queues = M.insert rId c {senderKey = Just sKey} (queues cs)})
_ -> (Right c, cs {queues = M.insert rId c {senderKey = Just sKey} (queues cs)})
addQueueNotifier :: QueueStore -> RecipientId -> NotifierId -> NtfPublicVerifyKey -> STM (Either ErrorType QueueRec)
addQueueNotifier store rId nId nKey = do
cs@QueueStoreData {queues, notifiers} <- readTVar store
if M.member nId notifiers
then pure $ Left DUPLICATE_
else case M.lookup rId queues of
Nothing -> pure $ Left AUTH
Just q -> case notifier q of
Just _ -> pure $ Left AUTH
_ -> do
writeTVar store $
cs
{ queues = M.insert rId q {notifier = Just (nId, nKey)} queues,
notifiers = M.insert nId rId notifiers
}
pure $ Right q
suspendQueue :: QueueStore -> RecipientId -> STM (Either ErrorType ())
suspendQueue store rId =
@@ -76,8 +101,8 @@ instance MonadQueueStore QueueStore STM where
updateQueues ::
QueueStore ->
RecipientId ->
(QueueStoreData -> QueueRec -> (Either ErrorType (), QueueStoreData)) ->
STM (Either ErrorType ())
(QueueStoreData -> QueueRec -> (Either ErrorType a, QueueStoreData)) ->
STM (Either ErrorType a)
updateQueues store rId update = do
cs <- readTVar store
let conn = getRcpQueue cs rId
+45 -36
View File
@@ -5,6 +5,7 @@
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Simplex.Messaging.Server.StoreLog
( StoreLog, -- constructors are not exported
@@ -14,6 +15,7 @@ module Simplex.Messaging.Server.StoreLog
closeStoreLog,
logCreateQueue,
logSecureQueue,
logAddNotifier,
logDeleteQueue,
readWriteStoreLog,
)
@@ -21,10 +23,7 @@ where
import Control.Applicative (optional, (<|>))
import Control.Monad (unless)
import Data.Attoparsec.ByteString.Char8 (Parser)
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.Bifunctor (first, second)
import Data.ByteString.Base64 (encode)
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as LB
@@ -33,8 +32,7 @@ import Data.Functor (($>))
import Data.List (foldl')
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Parsers (base64P, parseAll)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol
import Simplex.Messaging.Server.QueueStore (QueueRec (..), QueueStatus (..))
import Simplex.Messaging.Transport (trimCR)
@@ -49,37 +47,44 @@ data StoreLog (a :: IOMode) where
data StoreLogRecord
= CreateQueue QueueRec
| SecureQueue QueueId SenderPublicKey
| SecureQueue QueueId SndPublicVerifyKey
| AddNotifier QueueId NotifierId NtfPublicVerifyKey
| DeleteQueue QueueId
storeLogRecordP :: Parser StoreLogRecord
storeLogRecordP =
"CREATE " *> createQueueP
<|> "SECURE " *> secureQueueP
<|> "DELETE " *> (DeleteQueue <$> base64P)
where
createQueueP = CreateQueue <$> queueRecP
secureQueueP = SecureQueue <$> base64P <* A.space <*> C.pubKeyP
queueRecP = do
recipientId <- "rid=" *> base64P <* A.space
senderId <- "sid=" *> base64P <* A.space
recipientKey <- "rk=" *> C.pubKeyP <* A.space
senderKey <- "sk=" *> optional C.pubKeyP
pure QueueRec {recipientId, senderId, recipientKey, senderKey, status = QueueActive}
instance StrEncoding QueueRec where
strEncode QueueRec {recipientId, recipientKey, rcvDhSecret, senderId, senderKey, notifier} =
B.unwords
[ "rid=" <> strEncode recipientId,
"rk=" <> strEncode recipientKey,
"rdh=" <> strEncode rcvDhSecret,
"sid=" <> strEncode senderId,
"sk=" <> strEncode senderKey
]
<> maybe "" notifierStr notifier
where
notifierStr (nId, nKey) = " nid=" <> strEncode nId <> " nk=" <> strEncode nKey
serializeStoreLogRecord :: StoreLogRecord -> ByteString
serializeStoreLogRecord = \case
CreateQueue q -> "CREATE " <> serializeQueue q
SecureQueue rId sKey -> "SECURE " <> encode rId <> " " <> C.serializePubKey sKey
DeleteQueue rId -> "DELETE " <> encode rId
where
serializeQueue QueueRec {recipientId, senderId, recipientKey, senderKey} =
B.unwords
[ "rid=" <> encode recipientId,
"sid=" <> encode senderId,
"rk=" <> C.serializePubKey recipientKey,
"sk=" <> maybe "" C.serializePubKey senderKey
]
strP = do
recipientId <- "rid=" *> strP_
recipientKey <- "rk=" *> strP_
rcvDhSecret <- "rdh=" *> strP_
senderId <- "sid=" *> strP_
senderKey <- "sk=" *> strP
notifier <- optional $ (,) <$> (" nid=" *> strP_) <*> ("nk=" *> strP)
pure QueueRec {recipientId, recipientKey, rcvDhSecret, senderId, senderKey, notifier, status = QueueActive}
instance StrEncoding StoreLogRecord where
strEncode = \case
CreateQueue q -> strEncode (Str "CREATE", q)
SecureQueue rId sKey -> strEncode (Str "SECURE", rId, sKey)
AddNotifier rId nId nKey -> strEncode (Str "NOTIFIER", rId, nId, nKey)
DeleteQueue rId -> strEncode (Str "DELETE", rId)
strP =
"CREATE " *> (CreateQueue <$> strP)
<|> "SECURE " *> (SecureQueue <$> strP_ <*> strP)
<|> "NOTIFIER " *> (AddNotifier <$> strP_ <*> strP_ <*> strP)
<|> "DELETE " *> (DeleteQueue <$> strP)
openWriteStoreLog :: FilePath -> IO (StoreLog 'WriteMode)
openWriteStoreLog f = WriteStoreLog f <$> openFile f WriteMode
@@ -101,15 +106,18 @@ closeStoreLog = \case
writeStoreLogRecord :: StoreLog 'WriteMode -> StoreLogRecord -> IO ()
writeStoreLogRecord (WriteStoreLog _ h) r = do
B.hPutStrLn h $ serializeStoreLogRecord r
B.hPutStrLn h $ strEncode r
hFlush h
logCreateQueue :: StoreLog 'WriteMode -> QueueRec -> IO ()
logCreateQueue s = writeStoreLogRecord s . CreateQueue
logSecureQueue :: StoreLog 'WriteMode -> QueueId -> SenderPublicKey -> IO ()
logSecureQueue :: StoreLog 'WriteMode -> QueueId -> SndPublicVerifyKey -> IO ()
logSecureQueue s qId sKey = writeStoreLogRecord s $ SecureQueue qId sKey
logAddNotifier :: StoreLog 'WriteMode -> QueueId -> NotifierId -> NtfPublicVerifyKey -> IO ()
logAddNotifier s qId nId nKey = writeStoreLogRecord s $ AddNotifier qId nId nKey
logDeleteQueue :: StoreLog 'WriteMode -> QueueId -> IO ()
logDeleteQueue s = writeStoreLogRecord s . DeleteQueue
@@ -136,11 +144,12 @@ readQueues (ReadStoreLog _ h) = LB.hGetContents h >>= returnResult . procStoreLo
returnResult :: ([LogParsingError], Map RecipientId QueueRec) -> IO (Map RecipientId QueueRec)
returnResult (errs, res) = mapM_ printError errs $> res
parseLogRecord :: LB.ByteString -> Either LogParsingError StoreLogRecord
parseLogRecord = (\s -> first (,s) $ parseAll storeLogRecordP s) . trimCR . LB.toStrict
parseLogRecord = (\s -> first (,s) $ strDecode s) . trimCR . LB.toStrict
procLogRecord :: Map RecipientId QueueRec -> StoreLogRecord -> Map RecipientId QueueRec
procLogRecord m = \case
CreateQueue q -> M.insert (recipientId q) q m
SecureQueue qId sKey -> M.adjust (\q -> q {senderKey = Just sKey}) qId m
AddNotifier qId nId nKey -> M.adjust (\q -> q {notifier = Just (nId, nKey)}) qId m
DeleteQueue qId -> M.delete qId m
printError :: LogParsingError -> IO ()
printError (e, s) = B.putStrLn $ "Error parsing log: " <> B.pack e <> " - " <> s
+229 -333
View File
@@ -1,10 +1,11 @@
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
@@ -24,25 +25,31 @@
--
-- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#appendix-a
module Simplex.Messaging.Transport
( -- * Transport connection class
( -- * SMP transport parameters
smpBlockSize,
supportedSMPVersions,
simplexMQVersion,
-- * Transport connection class
Transport (..),
TProxy (..),
ATransport (..),
TransportPeer (..),
-- * Transport over TCP
runTransportServer,
runTransportClient,
-- * TLS Transport
TLS (..),
connectTLS,
closeTLS,
supportedParameters,
withTlsUnique,
-- * TCP transport
TCP (..),
-- * SMP encrypted transport
-- * SMP transport
THandle (..),
TransportError (..),
serverHandshake,
clientHandshake,
tPutEncrypted,
tGetEncrypted,
tPutBlock,
tGetBlock,
serializeTransportError,
transportErrorP,
@@ -53,36 +60,44 @@ where
import Control.Applicative ((<|>))
import Control.Monad.Except
import Control.Monad.IO.Unlift
import Control.Monad.Trans.Except (throwE)
import Crypto.Cipher.Types (AuthTag)
import Data.Aeson (ToJSON)
import qualified Data.Aeson as J
import Data.Attoparsec.ByteString.Char8 (Parser)
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.Bifunctor (first)
import Data.ByteArray (xor)
import Data.Bitraversable (bimapM)
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy as BL
import Data.Default (def)
import Data.Functor (($>))
import Data.Set (Set)
import qualified Data.Set as S
import Data.Word (Word32)
import GHC.Generics (Generic)
import GHC.IO.Exception (IOErrorType (..))
import GHC.IO.Handle.Internals (ioe_EOF)
import Generic.Random (genericArbitraryU)
import Network.Socket
import Network.Transport.Internal (decodeNum16, decodeNum32, encodeEnum16, encodeEnum32, encodeWord32)
import qualified Network.TLS as T
import qualified Network.TLS.Extra as TE
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Parsers (parse, parseAll, parseRead1)
import Simplex.Messaging.Util (bshow, liftError)
import System.IO
import System.IO.Error
import Simplex.Messaging.Encoding
import Simplex.Messaging.Parsers (dropPrefix, parse, parseRead1, sumTypeJSON)
import Simplex.Messaging.Util (bshow)
import Simplex.Messaging.Version
import Test.QuickCheck (Arbitrary (..))
import UnliftIO.Concurrent
import UnliftIO.Exception (Exception, IOException)
import UnliftIO.Exception (Exception)
import qualified UnliftIO.Exception as E
import UnliftIO.STM
-- * Transport parameters
smpBlockSize :: Int
smpBlockSize = 16384
supportedSMPVersions :: VersionRange
supportedSMPVersions = mkVersionRange 1 1
simplexMQVersion :: String
simplexMQVersion = "1.0.3"
-- * Transport connection class
class Transport c where
@@ -91,11 +106,16 @@ class Transport c where
transportName :: TProxy c -> String
-- | Upgrade client socket to connection (used in the server)
getServerConnection :: Socket -> IO c
transportPeer :: c -> TransportPeer
-- | Upgrade server socket to connection (used in the client)
getClientConnection :: Socket -> IO c
-- | Upgrade server TLS context to connection (used in the server)
getServerConnection :: T.Context -> IO c
-- | Upgrade client TLS context to connection (used in the client)
getClientConnection :: T.Context -> IO c
-- | tls-unique channel binding per RFC5929
tlsUnique :: c -> ByteString
-- | Close connection
closeConnection :: c -> IO ()
@@ -113,173 +133,183 @@ class Transport c where
putLn :: c -> ByteString -> IO ()
putLn c = cPut c . (<> "\r\n")
data TransportPeer = TClient | TServer
deriving (Eq, Show)
data TProxy c = TProxy
data ATransport = forall c. Transport c => ATransport (TProxy c)
-- * Transport over TCP
-- * TLS Transport
-- | Run transport server (plain TCP or WebSockets) on passed TCP port and signal when server started and stopped via passed TMVar.
--
-- All accepted connections are passed to the passed function.
runTransportServer :: (Transport c, MonadUnliftIO m) => TMVar Bool -> ServiceName -> (c -> m ()) -> m ()
runTransportServer started port server = do
clients <- newTVarIO S.empty
E.bracket (liftIO $ startTCPServer started port) (liftIO . closeServer clients) \sock -> forever $ do
c <- liftIO $ acceptConnection sock
tid <- forkFinally (server c) (const $ liftIO $ closeConnection c)
atomically . modifyTVar clients $ S.insert tid
data TLS = TLS
{ tlsContext :: T.Context,
tlsPeer :: TransportPeer,
tlsUniq :: ByteString,
buffer :: TVar ByteString,
getLock :: TMVar ()
}
connectTLS :: T.TLSParams p => p -> Socket -> IO T.Context
connectTLS params sock =
E.bracketOnError (T.contextNew sock params) closeTLS $ \ctx -> do
T.handshake ctx
`E.catch` \(e :: E.SomeException) -> putStrLn ("exception: " <> show e) >> E.throwIO e
pure ctx
getTLS :: TransportPeer -> T.Context -> IO TLS
getTLS tlsPeer cxt = withTlsUnique tlsPeer cxt newTLS
where
closeServer :: TVar (Set ThreadId) -> Socket -> IO ()
closeServer clients sock = do
readTVarIO clients >>= mapM_ killThread
close sock
void . atomically $ tryPutTMVar started False
acceptConnection :: Transport c => Socket -> IO c
acceptConnection sock = accept sock >>= getServerConnection . fst
newTLS tlsUniq = do
buffer <- newTVarIO ""
getLock <- newTMVarIO ()
pure TLS {tlsContext = cxt, tlsPeer, tlsUniq, buffer, getLock}
startTCPServer :: TMVar Bool -> ServiceName -> IO Socket
startTCPServer started port = withSocketsDo $ resolve >>= open >>= setStarted
withTlsUnique :: TransportPeer -> T.Context -> (ByteString -> IO c) -> IO c
withTlsUnique peer cxt f =
cxtFinished peer cxt
>>= maybe (closeTLS cxt >> ioe_EOF) f
where
resolve =
let hints = defaultHints {addrFlags = [AI_PASSIVE], addrSocketType = Stream}
in head <$> getAddrInfo (Just hints) Nothing (Just port)
open addr = do
sock <- socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr)
setSocketOption sock ReuseAddr 1
withFdSocket sock setCloseOnExecIfNeeded
bind sock $ addrAddress addr
listen sock 1024
return sock
setStarted sock = atomically (tryPutTMVar started True) >> pure sock
cxtFinished TServer = T.getPeerFinished
cxtFinished TClient = T.getFinished
-- | Connect to passed TCP host:port and pass handle to the client.
runTransportClient :: Transport c => MonadUnliftIO m => HostName -> ServiceName -> (c -> m a) -> m a
runTransportClient host port client = do
c <- liftIO $ startTCPClient host port
client c `E.finally` liftIO (closeConnection c)
closeTLS :: T.Context -> IO ()
closeTLS ctx =
(T.bye ctx >> T.contextClose ctx) -- sometimes socket was closed before 'TLS.bye'
`E.catch` (\(_ :: E.SomeException) -> pure ()) -- so we catch the 'Broken pipe' error here
startTCPClient :: forall c. Transport c => HostName -> ServiceName -> IO c
startTCPClient host port = withSocketsDo $ resolve >>= tryOpen err
where
err :: IOException
err = mkIOError NoSuchThing "no address" Nothing Nothing
supportedParameters :: T.Supported
supportedParameters =
def
{ T.supportedVersions = [T.TLS13, T.TLS12],
T.supportedCiphers =
[ TE.cipher_TLS13_CHACHA20POLY1305_SHA256, -- for TLS13
TE.cipher_ECDHE_ECDSA_CHACHA20POLY1305_SHA256 -- for TLS12
],
T.supportedHashSignatures = [(T.HashIntrinsic, T.SignatureEd448), (T.HashIntrinsic, T.SignatureEd25519)],
T.supportedSecureRenegotiation = False,
T.supportedGroups = [T.X448, T.X25519]
}
resolve :: IO [AddrInfo]
resolve =
let hints = defaultHints {addrSocketType = Stream}
in getAddrInfo (Just hints) (Just host) (Just port)
instance Transport TLS where
transportName _ = "TLS"
transportPeer = tlsPeer
getServerConnection = getTLS TServer
getClientConnection = getTLS TClient
tlsUnique = tlsUniq
closeConnection tls = closeTLS $ tlsContext tls
tryOpen :: IOException -> [AddrInfo] -> IO c
tryOpen e [] = E.throwIO e
tryOpen _ (addr : as) =
E.try (open addr) >>= either (`tryOpen` as) pure
cGet :: TLS -> Int -> IO ByteString
cGet TLS {tlsContext, buffer, getLock} n =
E.bracket_
(atomically $ takeTMVar getLock)
(atomically $ putTMVar getLock ())
$ do
b <- readChunks =<< readTVarIO buffer
let (s, b') = B.splitAt n b
atomically $ writeTVar buffer b'
pure s
where
readChunks :: ByteString -> IO ByteString
readChunks b
| B.length b >= n = pure b
| otherwise = readChunks . (b <>) =<< T.recvData tlsContext `E.catch` handleEOF
handleEOF = \case
T.Error_EOF -> E.throwIO TEBadBlock
e -> E.throwIO e
open :: AddrInfo -> IO c
open addr = do
sock <- socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr)
connect sock $ addrAddress addr
getClientConnection sock
cPut :: TLS -> ByteString -> IO ()
cPut tls = T.sendData (tlsContext tls) . BL.fromStrict
-- * TCP transport
newtype TCP = TCP {tcpHandle :: Handle}
instance Transport TCP where
transportName _ = "TCP"
getServerConnection = fmap TCP . getSocketHandle
getClientConnection = getServerConnection
closeConnection = hClose . tcpHandle
cGet = B.hGet . tcpHandle
cPut = B.hPut . tcpHandle
getLn = fmap trimCR . B.hGetLine . tcpHandle
getSocketHandle :: Socket -> IO Handle
getSocketHandle conn = do
h <- socketToHandle conn ReadWriteMode
hSetBinaryMode h True
hSetNewlineMode h NewlineMode {inputNL = CRLF, outputNL = CRLF}
hSetBuffering h LineBuffering
return h
getLn :: TLS -> IO ByteString
getLn TLS {tlsContext, buffer, getLock} = do
E.bracket_
(atomically $ takeTMVar getLock)
(atomically $ putTMVar getLock ())
$ do
b <- readChunks =<< readTVarIO buffer
let (s, b') = B.break (== '\n') b
atomically $ writeTVar buffer (B.drop 1 b') -- drop '\n' we made a break at
pure $ trimCR s
where
readChunks :: ByteString -> IO ByteString
readChunks b
| B.elem '\n' b = pure b
| otherwise = readChunks . (b <>) =<< T.recvData tlsContext `E.catch` handleEOF
handleEOF = \case
T.Error_EOF -> E.throwIO TEBadBlock
e -> E.throwIO e
-- | Trim trailing CR from ByteString.
trimCR :: ByteString -> ByteString
trimCR "" = ""
trimCR s = if B.last s == '\r' then B.init s else s
-- * SMP encrypted transport
data SMPVersion = SMPVersion Int Int Int Int
deriving (Eq, Ord)
major :: SMPVersion -> (Int, Int)
major (SMPVersion a b _ _) = (a, b)
currentSMPVersion :: SMPVersion
currentSMPVersion = SMPVersion 0 3 2 0
serializeSMPVersion :: SMPVersion -> ByteString
serializeSMPVersion (SMPVersion a b c d) = B.intercalate "." [bshow a, bshow b, bshow c, bshow d]
smpVersionP :: Parser SMPVersion
smpVersionP =
let ver = A.decimal <* A.char '.'
in SMPVersion <$> ver <*> ver <*> ver <*> A.decimal
-- * SMP transport
-- | The handle for SMP encrypted transport connection over Transport .
data THandle c = THandle
{ connection :: c,
sndKey :: SessionKey,
rcvKey :: SessionKey,
blockSize :: Int
sessionId :: ByteString,
-- | agreed SMP server protocol version
smpVersion :: Version
}
data SessionKey = SessionKey
{ aesKey :: C.Key,
baseIV :: C.IV,
counter :: TVar Word32
data ServerHandshake = ServerHandshake
{ smpVersionRange :: VersionRange,
sessionId :: ByteString
}
data ClientHandshake = ClientHandshake
{ blockSize :: Int,
sndKey :: SessionKey,
rcvKey :: SessionKey
{ -- | agreed SMP server protocol version
smpVersion :: Version,
-- | server identity - CA certificate fingerprint
keyHash :: C.KeyHash
}
instance Encoding ClientHandshake where
smpEncode ClientHandshake {smpVersion, keyHash} = smpEncode (smpVersion, keyHash)
smpP = do
(smpVersion, keyHash) <- smpP
pure ClientHandshake {smpVersion, keyHash}
instance Encoding ServerHandshake where
smpEncode ServerHandshake {smpVersionRange, sessionId} =
smpEncode (smpVersionRange, sessionId)
smpP = do
(smpVersionRange, sessionId) <- smpP
pure ServerHandshake {smpVersionRange, sessionId}
-- | Error of SMP encrypted transport over TCP.
data TransportError
= -- | error parsing transport block
TEBadBlock
| -- | block encryption error
TEEncrypt
| -- | block decryption error
TEDecrypt
| -- | message does not fit in transport block
TELargeMsg
| -- | incorrect session ID
TEBadSession
| -- | transport handshake error
TEHandshake HandshakeError
TEHandshake {handshakeErr :: HandshakeError}
deriving (Eq, Generic, Read, Show, Exception)
instance ToJSON TransportError where
toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "TE"
toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "TE"
-- | Transport handshake error.
data HandshakeError
= -- | encryption error
ENCRYPT
| -- | decryption error
DECRYPT
| -- | error parsing protocol version
= -- | parsing error
PARSE
| -- | incompatible peer version
VERSION
| -- | error parsing RSA key
RSA_KEY
| -- | error parsing server transport header or invalid block size
HEADER
| -- | error parsing AES keys
AES_KEYS
| -- | not matching RSA key hash
BAD_HASH
| -- | lower major agent version than protocol version
MAJOR_VERSION
| -- | TCP transport terminated
TERMINATED
| -- | incorrect server identity
IDENTITY
deriving (Eq, Generic, Read, Show, Exception)
instance ToJSON HandshakeError where
toJSON = J.genericToJSON $ sumTypeJSON id
toEncoding = J.genericToEncoding $ sumTypeJSON id
instance Arbitrary TransportError where arbitrary = genericArbitraryU
instance Arbitrary HandshakeError where arbitrary = genericArbitraryU
@@ -288,201 +318,67 @@ instance Arbitrary HandshakeError where arbitrary = genericArbitraryU
transportErrorP :: Parser TransportError
transportErrorP =
"BLOCK" $> TEBadBlock
<|> "AES_ENCRYPT" $> TEEncrypt
<|> "AES_DECRYPT" $> TEDecrypt
<|> "LARGE_MSG" $> TELargeMsg
<|> "SESSION" $> TEBadSession
<|> TEHandshake <$> parseRead1
-- | Serialize SMP encrypted transport error.
serializeTransportError :: TransportError -> ByteString
serializeTransportError = \case
TEEncrypt -> "AES_ENCRYPT"
TEDecrypt -> "AES_DECRYPT"
TEBadBlock -> "BLOCK"
TELargeMsg -> "LARGE_MSG"
TEBadSession -> "SESSION"
TEHandshake e -> bshow e
-- | Encrypt and send block to SMP encrypted transport.
tPutEncrypted :: Transport c => THandle c -> ByteString -> IO (Either TransportError ())
tPutEncrypted THandle {connection = c, sndKey, blockSize} block =
encryptBlock sndKey (blockSize - C.authTagSize) block >>= \case
Left _ -> pure $ Left TEEncrypt
Right (authTag, msg) -> Right <$> cPut c (C.authTagToBS authTag <> msg)
-- | Pad and send block to SMP transport.
tPutBlock :: Transport c => THandle c -> ByteString -> IO (Either TransportError ())
tPutBlock THandle {connection = c} block =
bimapM (const $ pure TELargeMsg) (cPut c) $
C.pad block smpBlockSize
-- | Receive and decrypt block from SMP encrypted transport.
tGetEncrypted :: Transport c => THandle c -> IO (Either TransportError ByteString)
tGetEncrypted THandle {connection = c, rcvKey, blockSize} =
cGet c blockSize >>= decryptBlock rcvKey >>= \case
Left _ -> pure $ Left TEDecrypt
Right "" -> ioe_EOF
Right msg -> pure $ Right msg
-- | Receive block from SMP transport.
tGetBlock :: Transport c => THandle c -> IO (Either TransportError ByteString)
tGetBlock THandle {connection = c} =
cGet c smpBlockSize >>= \case
"" -> ioe_EOF
msg -> pure . first (const TELargeMsg) $ C.unPad msg
encryptBlock :: SessionKey -> Int -> ByteString -> IO (Either C.CryptoError (AuthTag, ByteString))
encryptBlock k@SessionKey {aesKey} size block = do
ivBytes <- makeNextIV k
runExceptT $ C.encryptAES aesKey ivBytes size block
decryptBlock :: SessionKey -> ByteString -> IO (Either C.CryptoError ByteString)
decryptBlock k@SessionKey {aesKey} block = do
let (authTag, msg') = B.splitAt C.authTagSize block
ivBytes <- makeNextIV k
runExceptT $ C.decryptAES aesKey ivBytes msg' (C.bsToAuthTag authTag)
makeNextIV :: SessionKey -> IO C.IV
makeNextIV SessionKey {baseIV, counter} = atomically $ do
c <- readTVar counter
writeTVar counter $ c + 1
pure $ iv c
where
(start, rest) = B.splitAt 4 $ C.unIV baseIV
iv c = C.IV $ (start `xor` encodeWord32 c) <> rest
-- | Server SMP encrypted transport handshake.
-- | Server SMP transport handshake.
--
-- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#appendix-a
--
-- The numbers in function names refer to the steps in the document.
serverHandshake :: forall c. Transport c => c -> C.FullKeyPair -> ExceptT TransportError IO (THandle c)
serverHandshake c (k, pk) = do
liftIO sendHeaderAndPublicKey_1
encryptedKeys <- receiveEncryptedKeys_4
-- TODO server currently ignores blockSize returned by the client
-- this is reserved for future support of streams
ClientHandshake {blockSize = _, sndKey, rcvKey} <- decryptParseKeys_5 encryptedKeys
th <- liftIO $ transportHandle c rcvKey sndKey transportBlockSize -- keys are swapped here
sendWelcome_6 th
pure th
where
sendHeaderAndPublicKey_1 :: IO ()
sendHeaderAndPublicKey_1 = do
let sKey = C.encodePubKey k
header = ServerHeader {blockSize = transportBlockSize, keySize = B.length sKey}
cPut c $ binaryServerHeader header
cPut c sKey
receiveEncryptedKeys_4 :: ExceptT TransportError IO ByteString
receiveEncryptedKeys_4 =
liftIO (cGet c $ C.publicKeySize k) >>= \case
"" -> throwE $ TEHandshake TERMINATED
ks -> pure ks
decryptParseKeys_5 :: ByteString -> ExceptT TransportError IO ClientHandshake
decryptParseKeys_5 encKeys =
liftError (const $ TEHandshake DECRYPT) (C.decryptOAEP pk encKeys)
>>= liftEither . parseClientHandshake
sendWelcome_6 :: THandle c -> ExceptT TransportError IO ()
sendWelcome_6 th = ExceptT . tPutEncrypted th $ serializeSMPVersion currentSMPVersion <> " "
serverHandshake :: forall c. Transport c => c -> C.KeyHash -> ExceptT TransportError IO (THandle c)
serverHandshake c kh = do
let th@THandle {sessionId} = tHandle c
sendHandshake th $ ServerHandshake {sessionId, smpVersionRange = supportedSMPVersions}
getHandshake th >>= \case
ClientHandshake {smpVersion, keyHash}
| keyHash /= kh ->
throwE $ TEHandshake IDENTITY
| smpVersion `isCompatible` supportedSMPVersions -> do
pure (th :: THandle c) {smpVersion}
| otherwise -> throwE $ TEHandshake VERSION
-- | Client SMP encrypted transport handshake.
-- | Client SMP transport handshake.
--
-- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#appendix-a
--
-- The numbers in function names refer to the steps in the document.
clientHandshake :: forall c. Transport c => c -> Maybe C.KeyHash -> ExceptT TransportError IO (THandle c)
clientHandshake :: forall c. Transport c => c -> C.KeyHash -> ExceptT TransportError IO (THandle c)
clientHandshake c keyHash = do
(k, blkSize) <- getHeaderAndPublicKey_1_2
-- TODO currently client always uses the blkSize returned by the server
keys@ClientHandshake {sndKey, rcvKey} <- liftIO $ generateKeys_3 blkSize
sendEncryptedKeys_4 k keys
th <- liftIO $ transportHandle c sndKey rcvKey blkSize
getWelcome_6 th >>= checkVersion
pure th
where
getHeaderAndPublicKey_1_2 :: ExceptT TransportError IO (C.PublicKey, Int)
getHeaderAndPublicKey_1_2 = do
header <- liftIO (cGet c serverHeaderSize)
ServerHeader {blockSize, keySize} <- liftEither $ parse serverHeaderP (TEHandshake HEADER) header
when (blockSize < transportBlockSize || blockSize > maxTransportBlockSize) $
throwError $ TEHandshake HEADER
s <- liftIO $ cGet c keySize
maybe (pure ()) (validateKeyHash_2 s) keyHash
key <- liftEither $ parseKey s
pure (key, blockSize)
parseKey :: ByteString -> Either TransportError C.PublicKey
parseKey = first (const $ TEHandshake RSA_KEY) . parseAll C.binaryPubKeyP
validateKeyHash_2 :: ByteString -> C.KeyHash -> ExceptT TransportError IO ()
validateKeyHash_2 k (C.KeyHash kHash)
| C.sha256Hash k == kHash = pure ()
| otherwise = throwE $ TEHandshake BAD_HASH
generateKeys_3 :: Int -> IO ClientHandshake
generateKeys_3 blkSize = ClientHandshake blkSize <$> generateKey <*> generateKey
generateKey :: IO SessionKey
generateKey = do
aesKey <- C.randomAesKey
baseIV <- C.randomIV
pure SessionKey {aesKey, baseIV, counter = undefined}
sendEncryptedKeys_4 :: C.PublicKey -> ClientHandshake -> ExceptT TransportError IO ()
sendEncryptedKeys_4 k keys =
liftError (const $ TEHandshake ENCRYPT) (C.encryptOAEP k $ serializeClientHandshake keys)
>>= liftIO . cPut c
getWelcome_6 :: THandle c -> ExceptT TransportError IO SMPVersion
getWelcome_6 th = ExceptT $ (>>= parseSMPVersion) <$> tGetEncrypted th
parseSMPVersion :: ByteString -> Either TransportError SMPVersion
parseSMPVersion = first (const $ TEHandshake VERSION) . A.parseOnly (smpVersionP <* A.space)
checkVersion :: SMPVersion -> ExceptT TransportError IO ()
checkVersion smpVersion =
when (major smpVersion > major currentSMPVersion) . throwE $
TEHandshake MAJOR_VERSION
let th@THandle {sessionId} = tHandle c
ServerHandshake {sessionId = sessId, smpVersionRange} <- getHandshake th
if sessionId /= sessId
then throwE TEBadSession
else case smpVersionRange `compatibleVersion` supportedSMPVersions of
Just (Compatible smpVersion) -> do
sendHandshake th $ ClientHandshake {smpVersion, keyHash}
pure (th :: THandle c) {smpVersion}
Nothing -> throwE $ TEHandshake VERSION
data ServerHeader = ServerHeader {blockSize :: Int, keySize :: Int}
deriving (Eq, Show)
sendHandshake :: (Transport c, Encoding smp) => THandle c -> smp -> ExceptT TransportError IO ()
sendHandshake th = ExceptT . tPutBlock th . smpEncode
binaryRsaTransport :: Int
binaryRsaTransport = 0
getHandshake :: (Transport c, Encoding smp) => THandle c -> ExceptT TransportError IO smp
getHandshake th = ExceptT $ (parse smpP (TEHandshake PARSE) =<<) <$> tGetBlock th
transportBlockSize :: Int
transportBlockSize = 4096
maxTransportBlockSize :: Int
maxTransportBlockSize = 65536
serverHeaderSize :: Int
serverHeaderSize = 8
binaryServerHeader :: ServerHeader -> ByteString
binaryServerHeader ServerHeader {blockSize, keySize} =
encodeEnum32 blockSize <> encodeEnum16 binaryRsaTransport <> encodeEnum16 keySize
serverHeaderP :: Parser ServerHeader
serverHeaderP = ServerHeader <$> int32 <* binaryRsaTransportP <*> int16
serializeClientHandshake :: ClientHandshake -> ByteString
serializeClientHandshake ClientHandshake {blockSize, sndKey, rcvKey} =
encodeEnum32 blockSize <> encodeEnum16 binaryRsaTransport <> serializeKey sndKey <> serializeKey rcvKey
where
serializeKey :: SessionKey -> ByteString
serializeKey SessionKey {aesKey, baseIV} = C.unKey aesKey <> C.unIV baseIV
clientHandshakeP :: Parser ClientHandshake
clientHandshakeP = ClientHandshake <$> int32 <* binaryRsaTransportP <*> keyP <*> keyP
where
keyP :: Parser SessionKey
keyP = do
aesKey <- C.aesKeyP
baseIV <- C.ivP
pure SessionKey {aesKey, baseIV, counter = undefined}
int32 :: Parser Int
int32 = decodeNum32 <$> A.take 4
int16 :: Parser Int
int16 = decodeNum16 <$> A.take 2
binaryRsaTransportP :: Parser ()
binaryRsaTransportP = binaryRsa =<< int16
where
binaryRsa :: Int -> Parser ()
binaryRsa n
| n == binaryRsaTransport = pure ()
| otherwise = fail "unknown transport mode"
parseClientHandshake :: ByteString -> Either TransportError ClientHandshake
parseClientHandshake = parse clientHandshakeP $ TEHandshake AES_KEYS
transportHandle :: c -> SessionKey -> SessionKey -> Int -> IO (THandle c)
transportHandle c sk rk blockSize = do
sndCounter <- newTVarIO 0
rcvCounter <- newTVarIO 0
pure
THandle
{ connection = c,
sndKey = sk {counter = sndCounter},
rcvKey = rk {counter = rcvCounter},
blockSize
}
tHandle :: Transport c => c -> THandle c
tHandle c =
THandle {connection = c, sessionId = tlsUnique c, smpVersion = 0}
+82
View File
@@ -0,0 +1,82 @@
{-# LANGUAGE ScopedTypeVariables #-}
module Simplex.Messaging.Transport.Client
( runTransportClient,
clientHandshake,
)
where
import Control.Monad.Except
import Control.Monad.IO.Unlift
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Default (def)
import qualified Data.X509 as X
import qualified Data.X509.CertificateStore as XS
import Data.X509.Validation (Fingerprint (..))
import qualified Data.X509.Validation as XV
import GHC.IO.Exception (IOErrorType (..))
import Network.Socket
import qualified Network.TLS as T
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Transport
import System.IO.Error
import UnliftIO.Exception (IOException)
import qualified UnliftIO.Exception as E
-- | Connect to passed TCP host:port and pass handle to the client.
runTransportClient :: Transport c => MonadUnliftIO m => HostName -> ServiceName -> C.KeyHash -> (c -> m a) -> m a
runTransportClient host port keyHash client = do
let clientParams = mkTLSClientParams host port keyHash
c <- liftIO $ startTCPClient host port clientParams
client c `E.finally` liftIO (closeConnection c)
startTCPClient :: forall c. Transport c => HostName -> ServiceName -> T.ClientParams -> IO c
startTCPClient host port clientParams = withSocketsDo $ resolve >>= tryOpen err
where
err :: IOException
err = mkIOError NoSuchThing "no address" Nothing Nothing
resolve :: IO [AddrInfo]
resolve =
let hints = defaultHints {addrSocketType = Stream}
in getAddrInfo (Just hints) (Just host) (Just port)
tryOpen :: IOException -> [AddrInfo] -> IO c
tryOpen e [] = E.throwIO e
tryOpen _ (addr : as) =
E.try (open addr) >>= either (`tryOpen` as) pure
open :: AddrInfo -> IO c
open addr = do
sock <- socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr)
connect sock $ addrAddress addr
ctx <- connectTLS clientParams sock
getClientConnection ctx
mkTLSClientParams :: HostName -> ServiceName -> C.KeyHash -> T.ClientParams
mkTLSClientParams host port keyHash = do
let p = B.pack port
(T.defaultParamsClient host p)
{ T.clientShared = def,
T.clientHooks = def {T.onServerCertificate = \_ _ _ -> validateCertificateChain keyHash host p},
T.clientSupported = supportedParameters
}
validateCertificateChain :: C.KeyHash -> HostName -> ByteString -> X.CertificateChain -> IO [XV.FailedReason]
validateCertificateChain _ _ _ (X.CertificateChain []) = pure [XV.EmptyChain]
validateCertificateChain _ _ _ (X.CertificateChain [_]) = pure [XV.EmptyChain]
validateCertificateChain (C.KeyHash kh) host port cc@(X.CertificateChain sc@[_, caCert]) =
if Fingerprint kh == XV.getFingerprint caCert X.HashSHA256
then x509validate
else pure [XV.UnknownCA]
where
x509validate :: IO [XV.FailedReason]
x509validate = XV.validate X.HashSHA256 hooks checks certStore cache serviceID cc
where
hooks = XV.defaultHooks
checks = XV.defaultChecks
certStore = XS.makeCertificateStore sc
cache = XV.exceptionValidationCache [] -- we manually check fingerprint only of the identity certificate (ca.crt)
serviceID = (host, port)
validateCertificateChain _ _ _ _ = pure [XV.AuthorityTooDeep]
+94
View File
@@ -0,0 +1,94 @@
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Simplex.Messaging.Transport.Server
( runTransportServer,
loadTLSServerParams,
loadFingerprint,
serverHandshake,
)
where
import Control.Monad.Except
import Control.Monad.IO.Unlift
import qualified Crypto.Store.X509 as SX
import Data.Default (def)
import Data.Set (Set)
import qualified Data.Set as S
import qualified Data.X509 as X
import Data.X509.Validation (Fingerprint (..))
import qualified Data.X509.Validation as XV
import Network.Socket
import qualified Network.TLS as T
import Simplex.Messaging.Transport
import System.Exit (exitFailure)
import UnliftIO.Concurrent
import qualified UnliftIO.Exception as E
import UnliftIO.STM
-- | Run transport server (plain TCP or WebSockets) on passed TCP port and signal when server started and stopped via passed TMVar.
--
-- All accepted connections are passed to the passed function.
runTransportServer :: forall c m. (Transport c, MonadUnliftIO m) => TMVar Bool -> ServiceName -> T.ServerParams -> (c -> m ()) -> m ()
runTransportServer started port serverParams server = do
u <- askUnliftIO
liftIO $ do
clients <- newTVarIO S.empty
E.bracket
(startTCPServer started port)
(closeServer clients)
$ \sock -> forever $ do
(connSock, _) <- accept sock
tid <- forkIO $ connectClient u connSock `E.catch` \(_ :: E.SomeException) -> pure ()
atomically . modifyTVar clients $ S.insert tid
where
connectClient :: UnliftIO m -> Socket -> IO ()
connectClient u connSock =
E.bracket
(connectTLS serverParams connSock >>= getServerConnection)
closeConnection
(unliftIO u . server)
closeServer :: TVar (Set ThreadId) -> Socket -> IO ()
closeServer clients sock = do
readTVarIO clients >>= mapM_ killThread
close sock
void . atomically $ tryPutTMVar started False
startTCPServer :: TMVar Bool -> ServiceName -> IO Socket
startTCPServer started port = withSocketsDo $ resolve >>= open >>= setStarted
where
resolve =
let hints = defaultHints {addrFlags = [AI_PASSIVE], addrSocketType = Stream}
in head <$> getAddrInfo (Just hints) Nothing (Just port)
open addr = do
sock <- socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr)
setSocketOption sock ReuseAddr 1
withFdSocket sock setCloseOnExecIfNeeded
bind sock $ addrAddress addr
listen sock 1024
return sock
setStarted sock = atomically (tryPutTMVar started True) >> pure sock
loadTLSServerParams :: FilePath -> FilePath -> FilePath -> IO T.ServerParams
loadTLSServerParams caCertificateFile certificateFile privateKeyFile =
fromCredential <$> loadServerCredential
where
loadServerCredential :: IO T.Credential
loadServerCredential =
T.credentialLoadX509Chain certificateFile [caCertificateFile] privateKeyFile >>= \case
Right credential -> pure credential
Left _ -> putStrLn "invalid credential" >> exitFailure
fromCredential :: T.Credential -> T.ServerParams
fromCredential credential =
def
{ T.serverWantClientCert = False,
T.serverShared = def {T.sharedCredentials = T.Credentials [credential]},
T.serverHooks = def,
T.serverSupported = supportedParameters
}
loadFingerprint :: FilePath -> IO Fingerprint
loadFingerprint certificateFile = do
(cert : _) <- SX.readSignedObject certificateFile
pure $ XV.getFingerprint (cert :: X.SignedExact X.Certificate) X.HashSHA256
+55 -18
View File
@@ -1,23 +1,39 @@
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
module Simplex.Messaging.Transport.WebSockets (WS (..)) where
import qualified Control.Exception as E
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Network.Socket (Socket)
import qualified Data.ByteString.Lazy as BL
import qualified Network.TLS as T
import Network.WebSockets
import Network.WebSockets.Stream (Stream)
import qualified Network.WebSockets.Stream as S
import Simplex.Messaging.Transport (TProxy, Transport (..), TransportError (..), trimCR)
import Simplex.Messaging.Transport
( TProxy,
Transport (..),
TransportError (..),
TransportPeer (..),
closeTLS,
trimCR,
withTlsUnique,
)
data WS = WS {wsStream :: Stream, wsConnection :: Connection}
data WS = WS
{ wsPeer :: TransportPeer,
tlsUniq :: ByteString,
wsStream :: Stream,
wsConnection :: Connection
}
websocketsOpts :: ConnectionOptions
websocketsOpts =
defaultConnectionOptions
{ connectionCompressionOptions = NoCompression,
connectionFramePayloadSizeLimit = SizeLimit 8192,
connectionFramePayloadSizeLimit = SizeLimit $ 16 * 1024, -- TODO move to Protocol
connectionMessageDataSizeLimit = SizeLimit 65536
}
@@ -25,21 +41,17 @@ instance Transport WS where
transportName :: TProxy WS -> String
transportName _ = "WebSockets"
getServerConnection :: Socket -> IO WS
getServerConnection sock = do
s <- S.makeSocketStream sock
WS s <$> acceptClientRequest s
where
acceptClientRequest :: Stream -> IO Connection
acceptClientRequest s = makePendingConnectionFromStream s websocketsOpts >>= acceptRequest
transportPeer :: WS -> TransportPeer
transportPeer = wsPeer
getClientConnection :: Socket -> IO WS
getClientConnection sock = do
s <- S.makeSocketStream sock
WS s <$> sendClientRequest s
where
sendClientRequest :: Stream -> IO Connection
sendClientRequest s = newClientConnection s "" "/" websocketsOpts []
getServerConnection :: T.Context -> IO WS
getServerConnection = getWS TServer
getClientConnection :: T.Context -> IO WS
getClientConnection = getWS TClient
tlsUnique :: WS -> ByteString
tlsUnique = tlsUniq
closeConnection :: WS -> IO ()
closeConnection = S.close . wsStream
@@ -60,3 +72,28 @@ instance Transport WS where
if B.null s || B.last s /= '\n'
then E.throwIO TEBadBlock
else pure $ B.init s
getWS :: TransportPeer -> T.Context -> IO WS
getWS wsPeer cxt = withTlsUnique wsPeer cxt connectWS
where
connectWS tlsUniq = do
s <- makeTLSContextStream cxt
wsConnection <- connectPeer wsPeer s
pure $ WS {wsPeer, tlsUniq, wsStream = s, wsConnection}
connectPeer :: TransportPeer -> Stream -> IO Connection
connectPeer TServer = acceptClientRequest
connectPeer TClient = sendClientRequest
acceptClientRequest s = makePendingConnectionFromStream s websocketsOpts >>= acceptRequest
sendClientRequest s = newClientConnection s "" "/" websocketsOpts []
makeTLSContextStream :: T.Context -> IO Stream
makeTLSContextStream cxt =
S.makeStream readStream writeStream
where
readStream :: IO (Maybe ByteString)
readStream =
(Just <$> T.recvData cxt) `E.catch` \case
T.Error_EOF -> pure Nothing
e -> E.throwIO e
writeStream :: Maybe BL.ByteString -> IO ()
writeStream = maybe (closeTLS cxt) (T.sendData cxt)
+29 -18
View File
@@ -1,29 +1,14 @@
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE RankNTypes #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE OverloadedStrings #-}
module Simplex.Messaging.Util where
import Control.Monad.Except
import Control.Monad.IO.Unlift
import Control.Monad.Trans.Except
import Data.Bifunctor (first)
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import UnliftIO.Async
import UnliftIO.Exception (Exception)
import qualified UnliftIO.Exception as E
newtype InternalException e = InternalException {unInternalException :: e}
deriving (Eq, Show)
instance Exception e => Exception (InternalException e)
instance (MonadUnliftIO m, Exception e) => MonadUnliftIO (ExceptT e m) where
withRunInIO :: ((forall a. ExceptT e m a -> IO a) -> IO b) -> ExceptT e m b
withRunInIO exceptToIO =
withExceptT unInternalException . ExceptT . E.try $
withRunInIO $ \run ->
exceptToIO $ run . (either (E.throwIO . InternalException) return <=< runExceptT)
raceAny_ :: MonadUnliftIO m => [m a] -> m ()
raceAny_ = r []
@@ -35,18 +20,44 @@ infixl 4 <$$>, <$?>
(<$$>) :: (Functor f, Functor g) => (a -> b) -> f (g a) -> f (g b)
(<$$>) = fmap . fmap
{-# INLINE (<$$>) #-}
(<$?>) :: MonadFail m => (a -> Either String b) -> m a -> m b
f <$?> m = m >>= either fail pure . f
f <$?> m = either fail pure . f =<< m
{-# INLINE (<$?>) #-}
bshow :: Show a => a -> ByteString
bshow = B.pack . show
{-# INLINE bshow #-}
maybeWord :: (a -> ByteString) -> Maybe a -> ByteString
maybeWord f = maybe "" $ B.cons ' ' . f
{-# INLINE maybeWord #-}
liftIOEither :: (MonadIO m, MonadError e m) => IO (Either e a) -> m a
liftIOEither a = liftIO a >>= liftEither
{-# INLINE liftIOEither #-}
liftError :: (MonadIO m, MonadError e' m) => (e -> e') -> ExceptT e IO a -> m a
liftError f = liftEitherError f . runExceptT
{-# INLINE liftError #-}
liftEitherError :: (MonadIO m, MonadError e' m) => (e -> e') -> IO (Either e a) -> m a
liftEitherError f a = liftIOEither (first f <$> a)
{-# INLINE liftEitherError #-}
tryError :: MonadError e m => m a -> m (Either e a)
tryError action = (Right <$> action) `catchError` (pure . Left)
{-# INLINE tryError #-}
tryE :: Monad m => ExceptT e m a -> ExceptT e m (Either e a)
tryE m = (Right <$> m) `catchE` (pure . Left)
{-# INLINE tryE #-}
ifM :: Monad m => m Bool -> m a -> m a -> m a
ifM ba t f = ba >>= \b -> if b then t else f
{-# INLINE ifM #-}
unlessM :: Monad m => m Bool -> m () -> m ()
unlessM b = ifM b $ pure ()
{-# INLINE unlessM #-}
+126
View File
@@ -0,0 +1,126 @@
{-# LANGUAGE ConstrainedClassMethods #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeSynonymInstances #-}
module Simplex.Messaging.Version
( Version,
VersionRange (minVersion, maxVersion),
pattern VersionRange,
VersionI (..),
VersionRangeI (..),
Compatible,
pattern Compatible,
mkVersionRange,
safeVersionRange,
isCompatible,
proveCompatible,
compatibleVersion,
)
where
import Control.Applicative (optional)
import Data.Aeson (FromJSON (..), ToJSON (..))
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.Word (Word16)
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Util ((<$?>))
pattern VersionRange :: Word16 -> Word16 -> VersionRange
pattern VersionRange v1 v2 <- VRange v1 v2
{-# COMPLETE VersionRange #-}
type Version = Word16
data VersionRange = VRange
{ minVersion :: Version,
maxVersion :: Version
}
deriving (Eq, Show)
-- | construct valid version range, to be used in constants
mkVersionRange :: Version -> Version -> VersionRange
mkVersionRange v1 v2
| v1 <= v2 = VRange v1 v2
| otherwise = error "invalid version range"
safeVersionRange :: Version -> Version -> Maybe VersionRange
safeVersionRange v1 v2
| v1 <= v2 = Just $ VRange v1 v2
| otherwise = Nothing
instance Encoding VersionRange where
smpEncode (VRange v1 v2) = smpEncode (v1, v2)
smpP =
maybe (fail "invalid version range") pure
=<< safeVersionRange <$> smpP <*> smpP
instance StrEncoding VersionRange where
strEncode (VRange v1 v2)
| v1 == v2 = strEncode v1
| otherwise = strEncode v1 <> "-" <> strEncode v2
strP = do
v1 <- strP
v2 <- maybe (pure v1) (const strP) =<< optional (A.char '-')
maybe (fail "invalid version range") pure $ safeVersionRange v1 v2
instance ToJSON VersionRange where
toJSON (VRange v1 v2) = toJSON (v1, v2)
toEncoding (VRange v1 v2) = toEncoding (v1, v2)
instance FromJSON VersionRange where
parseJSON v =
(\(v1, v2) -> maybe (Left "bad VersionRange") Right $ safeVersionRange v1 v2)
<$?> parseJSON v
class VersionI a where
type VersionRangeT a
version :: a -> Version
toVersionRangeT :: a -> VersionRange -> VersionRangeT a
class VersionRangeI a where
type VersionT a
versionRange :: a -> VersionRange
toVersionT :: a -> Version -> VersionT a
instance VersionI Version where
type VersionRangeT Version = VersionRange
version = id
toVersionRangeT _ vr = vr
instance VersionRangeI VersionRange where
type VersionT VersionRange = Version
versionRange = id
toVersionT _ v = v
newtype Compatible a = Compatible_ a
pattern Compatible :: a -> Compatible a
pattern Compatible a <- Compatible_ a
{-# COMPLETE Compatible #-}
isCompatible :: VersionI a => a -> VersionRange -> Bool
isCompatible x (VRange v1 v2) = let v = version x in v1 <= v && v <= v2
isCompatibleRange :: VersionRangeI a => a -> VersionRange -> Bool
isCompatibleRange x (VRange min2 max2) = min1 <= max2 && min2 <= max1
where
VRange min1 max1 = versionRange x
proveCompatible :: VersionI a => a -> VersionRange -> Maybe (Compatible a)
proveCompatible x vr = x `mkCompatibleIf` (x `isCompatible` vr)
compatibleVersion :: VersionRangeI a => a -> VersionRange -> Maybe (Compatible (VersionT a))
compatibleVersion x vr =
toVersionT x (min max1 max2) `mkCompatibleIf` isCompatibleRange x vr
where
max1 = maxVersion $ versionRange x
max2 = maxVersion vr
mkCompatibleIf :: a -> Bool -> Maybe (Compatible a)
x `mkCompatibleIf` cond = if cond then Just $ Compatible_ x else Nothing
+16 -8
View File
@@ -17,7 +17,7 @@
#
# resolver: ./custom-snapshot.yaml
# resolver: https://example.com/snapshots/2018-01-01.yaml
resolver: lts-17.12
resolver: lts-18.21
# User packages to be built.
# Various formats can be used as shown in the example below.
@@ -36,14 +36,22 @@ packages:
#
extra-deps:
- cryptostore-0.2.1.0@sha256:9896e2984f36a1c8790f057fd5ce3da4cbcaf8aa73eb2d9277916886978c5b19,3881
- direct-sqlite-2.3.26@sha256:04e835402f1508abca383182023e4e2b9b86297b8533afbd4e57d1a5652e0c23,3718
- simple-logger-0.1.0@sha256:be8ede4bd251a9cac776533bae7fb643369ebd826eb948a9a18df1a8dd252ff8,1079
- sqlite-simple-0.4.18.0@sha256:3ceea56375c0a3590c814e411a4eb86943f8d31b93b110ca159c90689b6b39e5,3002
# - network-run-0.2.4@sha256:7dbb06def522dab413bce4a46af476820bffdff2071974736b06f52f4ab57c96,885
# - git: https://github.com/commercialhaskell/stack.git
# commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a
#
# extra-deps: []
- tls-1.5.7@sha256:1cc30253a9696b65a9cafc0317fbf09f7dcea15e3a145ed6c9c0e28c632fa23a,6991
# below dependancies are to update Aeson to 2.0.3
- OneTuple-0.3.1@sha256:a848c096c9d29e82ffdd30a9998aa2931cbccb3a1bc137539d80f6174d31603e,2262
- attoparsec-0.14.4@sha256:79584bdada8b730cb5138fca8c35c76fbef75fc1d1e01e6b1d815a5ee9843191,5810
- hashable-1.4.0.2@sha256:0cddd0229d1aac305ea0404409c0bbfab81f075817bd74b8b2929eff58333e55,5005
- semialign-1.2.0.1@sha256:0e179b4d3a8eff79001d374d6c91917c6221696b9620f0a4d86852fc6a9b9501,2836
- text-short-0.1.5@sha256:962c6228555debdc46f758d0317dea16e5240d01419b42966674b08a5c3d8fa6,3498
- time-compat-1.9.6.1@sha256:42d8f2e08e965e1718917d54ad69e1d06bd4b87d66c41dc7410f59313dba4ed1,5033
- github: simplex-chat/aeson
commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7
# - ../hs-tls/core
# - github: simplex-chat/hs-tls
# commit: f6cc753611f80af300401cfae63846e9d7c40d9e
# subdirs:
# - core
# Override default flag values for local packages and extra-deps
# flags: {}
+383 -48
View File
@@ -1,50 +1,91 @@
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE PostfixOperators #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-}
module AgentTests where
module AgentTests (agentTests) where
import AgentTests.ConnectionRequestTests
import AgentTests.DoubleRatchetTests (doubleRatchetTests)
import AgentTests.FunctionalAPITests (functionalAPITests)
import AgentTests.SQLiteTests (storeTests)
import Control.Concurrent
import Control.Monad (forM_)
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Network.HTTP.Types (urlEncode)
import SMPAgentClient
import SMPClient (testPort, testPort2, testStoreLogFile, withSmpServer, withSmpServerStoreLogOn)
import Simplex.Messaging.Agent.Protocol
import qualified Simplex.Messaging.Agent.Protocol as A
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol (ErrorType (..), MsgBody)
import Simplex.Messaging.Transport (ATransport (..), TProxy (..), Transport (..))
import Simplex.Messaging.Util (bshow)
import System.Directory (removeFile)
import System.Timeout
import Test.Hspec
agentTests :: ATransport -> Spec
agentTests (ATransport t) = do
describe "Connection request" connectionRequestTests
describe "Double ratchet tests" doubleRatchetTests
describe "Functional API" $ functionalAPITests (ATransport t)
describe "SQLite store" storeTests
describe "SMP agent protocol syntax" $ syntaxTests t
describe "Establishing duplex connection" do
describe "Establishing duplex connection" $ do
it "should connect via one server and one agent" $
smpAgentTest2_1_1 $ testDuplexConnection t
it "should connect via one server and one agent (random IDs)" $
smpAgentTest2_1_1 $ testDuplexConnRandomIds t
it "should connect via one server and 2 agents" $
smpAgentTest2_2_1 $ testDuplexConnection t
it "should connect via one server and 2 agents (random IDs)" $
smpAgentTest2_2_1 $ testDuplexConnRandomIds t
it "should connect via 2 servers and 2 agents" $
smpAgentTest2_2_2 $ testDuplexConnection t
describe "Connection subscriptions" do
it "should connect via 2 servers and 2 agents (random IDs)" $
smpAgentTest2_2_2 $ testDuplexConnRandomIds t
describe "Establishing connections via `contact connection`" $ do
it "should connect via contact connection with one server and 3 agents" $
smpAgentTest3 $ testContactConnection t
it "should connect via contact connection with one server and 2 agents (random IDs)" $
smpAgentTest2_2_1 $ testContactConnRandomIds t
it "should support rejecting contact request" $
smpAgentTest2_2_1 $ testRejectContactRequest t
describe "Connection subscriptions" $ do
it "should connect via one server and one agent" $
smpAgentTest3_1_1 $ testSubscription t
it "should send notifications to client when server disconnects" $
smpAgentServerTest $ testSubscrNotification t
describe "Message delivery and server reconnection" $ do
it "should deliver messages after losing server connection and re-connecting" $
smpAgentTest2_2_2_needs_server $ testMsgDeliveryServerRestart t
it "should connect to the server when server goes up if it initially was down" $
smpAgentTestN [] $ testServerConnectionAfterError t
it "should deliver pending messages after agent restarting" $
smpAgentTest1_1_1 $ testMsgDeliveryAgentRestart t
it "should concurrently deliver messages to connections without blocking" $
smpAgentTest2_2_1 $ testConcurrentMsgDelivery t
it "should deliver messages if one of connections has quota exceeded" $
smpAgentTest2_2_1 $ testMsgDeliveryQuotaExceeded t
-- | receive message to handle `h`
(<#:) :: Transport c => c -> IO (ATransmissionOrError 'Agent)
(<#:) = tGet SAgent
-- | send transmission `t` to handle `h` and get response
(#:) :: Transport c => c -> (ByteString, ByteString, ByteString) -> IO (ATransmissionOrError 'Agent)
h #: t = tPutRaw h t >> tGet SAgent h
h #: t = tPutRaw h t >> (<#:) h
-- | action and expected response
-- `h #:t #> r` is the test that sends `t` to `h` and validates that the response is `r`
(#>) :: IO (ATransmissionOrError 'Agent) -> ATransmission 'Agent -> Expectation
action #> (corrId, cAlias, cmd) = action `shouldReturn` (corrId, cAlias, Right cmd)
action #> (corrId, connId, cmd) = action `shouldReturn` (corrId, connId, Right cmd)
-- | action and predicate for the response
-- `h #:t =#> p` is the test that sends `t` to `h` and validates the response using `p`
@@ -52,17 +93,17 @@ action #> (corrId, cAlias, cmd) = action `shouldReturn` (corrId, cAlias, Right c
action =#> p = action >>= (`shouldSatisfy` p . correctTransmission)
correctTransmission :: ATransmissionOrError a -> ATransmission a
correctTransmission (corrId, cAlias, cmdOrErr) = case cmdOrErr of
Right cmd -> (corrId, cAlias, cmd)
correctTransmission (corrId, connId, cmdOrErr) = case cmdOrErr of
Right cmd -> (corrId, connId, cmd)
Left e -> error $ show e
-- | receive message to handle `h` and validate that it is the expected one
(<#) :: Transport c => c -> ATransmission 'Agent -> Expectation
h <# (corrId, cAlias, cmd) = tGet SAgent h `shouldReturn` (corrId, cAlias, Right cmd)
h <# (corrId, connId, cmd) = (h <#:) `shouldReturn` (corrId, connId, Right cmd)
-- | receive message to handle `h` and validate it using predicate `p`
(<#=) :: Transport c => c -> (ATransmission 'Agent -> Bool) -> Expectation
h <#= p = tGet SAgent h >>= (`shouldSatisfy` p . correctTransmission)
h <#= p = (h <#:) >>= (`shouldSatisfy` p . correctTransmission)
-- | test that nothing is delivered to handle `h` during 10ms
(#:#) :: Transport c => c -> String -> Expectation
@@ -74,73 +115,367 @@ h #:# err = tryGet `shouldReturn` ()
_ -> return ()
pattern Msg :: MsgBody -> ACommand 'Agent
pattern Msg msgBody <- MSG {msgBody, msgIntegrity = MsgOk}
pattern Msg msgBody <- MSG MsgMeta {integrity = MsgOk} msgBody
testDuplexConnection :: Transport c => TProxy c -> c -> c -> IO ()
testDuplexConnection _ alice bob = do
("1", "bob", Right (INV qInfo)) <- alice #: ("1", "bob", "NEW")
let qInfo' = serializeSmpQueueInfo qInfo
bob #: ("11", "alice", "JOIN " <> qInfo') #> ("", "alice", CON)
("1", "bob", Right (INV cReq)) <- alice #: ("1", "bob", "NEW INV")
let cReq' = strEncode cReq
bob #: ("11", "alice", "JOIN " <> cReq' <> " 14\nbob's connInfo") #> ("11", "alice", OK)
("", "bob", Right (CONF confId "bob's connInfo")) <- (alice <#:)
alice #: ("2", "bob", "LET " <> confId <> " 16\nalice's connInfo") #> ("2", "bob", OK)
bob <# ("", "alice", INFO "alice's connInfo")
bob <# ("", "alice", CON)
alice <# ("", "bob", CON)
alice #: ("2", "bob", "SEND :hello") =#> \case ("2", "bob", SENT 1) -> True; _ -> False
alice #: ("3", "bob", "SEND :how are you?") =#> \case ("3", "bob", SENT 2) -> True; _ -> False
-- message IDs 1 to 3 get assigned to control messages, so first MSG is assigned ID 4
alice #: ("3", "bob", "SEND :hello") #> ("3", "bob", MID 4)
alice <# ("", "bob", SENT 4)
bob <#= \case ("", "alice", Msg "hello") -> True; _ -> False
bob #: ("12", "alice", "ACK 4") #> ("12", "alice", OK)
alice #: ("4", "bob", "SEND :how are you?") #> ("4", "bob", MID 5)
alice <# ("", "bob", SENT 5)
bob <#= \case ("", "alice", Msg "how are you?") -> True; _ -> False
bob #: ("14", "alice", "SEND 9\nhello too") =#> \case ("14", "alice", SENT 3) -> True; _ -> False
bob #: ("13", "alice", "ACK 5") #> ("13", "alice", OK)
bob #: ("14", "alice", "SEND 9\nhello too") #> ("14", "alice", MID 6)
bob <# ("", "alice", SENT 6)
alice <#= \case ("", "bob", Msg "hello too") -> True; _ -> False
bob #: ("15", "alice", "SEND 9\nmessage 1") =#> \case ("15", "alice", SENT 4) -> True; _ -> False
alice #: ("3a", "bob", "ACK 6") #> ("3a", "bob", OK)
bob #: ("15", "alice", "SEND 9\nmessage 1") #> ("15", "alice", MID 7)
bob <# ("", "alice", SENT 7)
alice <#= \case ("", "bob", Msg "message 1") -> True; _ -> False
alice #: ("4a", "bob", "ACK 7") #> ("4a", "bob", OK)
alice #: ("5", "bob", "OFF") #> ("5", "bob", OK)
bob #: ("17", "alice", "SEND 9\nmessage 3") #> ("17", "alice", ERR (SMP AUTH))
bob #: ("17", "alice", "SEND 9\nmessage 3") #> ("17", "alice", MID 8)
bob <# ("", "alice", MERR 8 (SMP AUTH))
alice #: ("6", "bob", "DEL") #> ("6", "bob", OK)
alice #:# "nothing else should be delivered to alice"
testDuplexConnRandomIds :: Transport c => TProxy c -> c -> c -> IO ()
testDuplexConnRandomIds _ alice bob = do
("1", bobConn, Right (INV cReq)) <- alice #: ("1", "", "NEW INV")
let cReq' = strEncode cReq
("11", aliceConn, Right OK) <- bob #: ("11", "", "JOIN " <> cReq' <> " 14\nbob's connInfo")
("", bobConn', Right (CONF confId "bob's connInfo")) <- (alice <#:)
bobConn' `shouldBe` bobConn
alice #: ("2", bobConn, "LET " <> confId <> " 16\nalice's connInfo") =#> \case ("2", c, OK) -> c == bobConn; _ -> False
bob <# ("", aliceConn, INFO "alice's connInfo")
bob <# ("", aliceConn, CON)
alice <# ("", bobConn, CON)
alice #: ("2", bobConn, "SEND :hello") #> ("2", bobConn, MID 4)
alice <# ("", bobConn, SENT 4)
bob <#= \case ("", c, Msg "hello") -> c == aliceConn; _ -> False
bob #: ("12", aliceConn, "ACK 4") #> ("12", aliceConn, OK)
alice #: ("3", bobConn, "SEND :how are you?") #> ("3", bobConn, MID 5)
alice <# ("", bobConn, SENT 5)
bob <#= \case ("", c, Msg "how are you?") -> c == aliceConn; _ -> False
bob #: ("13", aliceConn, "ACK 5") #> ("13", aliceConn, OK)
bob #: ("14", aliceConn, "SEND 9\nhello too") #> ("14", aliceConn, MID 6)
bob <# ("", aliceConn, SENT 6)
alice <#= \case ("", c, Msg "hello too") -> c == bobConn; _ -> False
alice #: ("3a", bobConn, "ACK 6") #> ("3a", bobConn, OK)
bob #: ("15", aliceConn, "SEND 9\nmessage 1") #> ("15", aliceConn, MID 7)
bob <# ("", aliceConn, SENT 7)
alice <#= \case ("", c, Msg "message 1") -> c == bobConn; _ -> False
alice #: ("4a", bobConn, "ACK 7") #> ("4a", bobConn, OK)
alice #: ("5", bobConn, "OFF") #> ("5", bobConn, OK)
bob #: ("17", aliceConn, "SEND 9\nmessage 3") #> ("17", aliceConn, MID 8)
bob <# ("", aliceConn, MERR 8 (SMP AUTH))
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' = strEncode cReq
bob #: ("11", "alice", "JOIN " <> cReq' <> " 14\nbob's connInfo") #> ("11", "alice", OK)
("", "alice_contact", Right (REQ aInvId "bob's connInfo")) <- (alice <#:)
alice #: ("2", "bob", "ACPT " <> aInvId <> " 16\nalice's connInfo") #> ("2", "bob", OK)
("", "alice", Right (CONF bConfId "alice's connInfo")) <- (bob <#:)
bob #: ("12", "alice", "LET " <> bConfId <> " 16\nbob's connInfo 2") #> ("12", "alice", OK)
alice <# ("", "bob", INFO "bob's connInfo 2")
alice <# ("", "bob", CON)
bob <# ("", "alice", CON)
alice #: ("3", "bob", "SEND :hi") #> ("3", "bob", MID 4)
alice <# ("", "bob", SENT 4)
bob <#= \case ("", "alice", Msg "hi") -> True; _ -> False
bob #: ("13", "alice", "ACK 4") #> ("13", "alice", OK)
tom #: ("21", "alice", "JOIN " <> cReq' <> " 14\ntom's connInfo") #> ("21", "alice", OK)
("", "alice_contact", Right (REQ aInvId' "tom's connInfo")) <- (alice <#:)
alice #: ("4", "tom", "ACPT " <> aInvId' <> " 16\nalice's connInfo") #> ("4", "tom", OK)
("", "alice", Right (CONF tConfId "alice's connInfo")) <- (tom <#:)
tom #: ("22", "alice", "LET " <> tConfId <> " 16\ntom's connInfo 2") #> ("22", "alice", OK)
alice <# ("", "tom", INFO "tom's connInfo 2")
alice <# ("", "tom", CON)
tom <# ("", "alice", CON)
alice #: ("5", "tom", "SEND :hi there") #> ("5", "tom", MID 4)
alice <# ("", "tom", SENT 4)
tom <#= \case ("", "alice", Msg "hi there") -> True; _ -> False
tom #: ("23", "alice", "ACK 4") #> ("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' = strEncode cReq
("11", aliceConn, Right OK) <- bob #: ("11", "", "JOIN " <> cReq' <> " 14\nbob's connInfo")
("", aliceContact', Right (REQ aInvId "bob's connInfo")) <- (alice <#:)
aliceContact' `shouldBe` aliceContact
("2", bobConn, Right OK) <- alice #: ("2", "", "ACPT " <> aInvId <> " 16\nalice's connInfo")
("", aliceConn', Right (CONF bConfId "alice's connInfo")) <- (bob <#:)
aliceConn' `shouldBe` aliceConn
bob #: ("12", aliceConn, "LET " <> bConfId <> " 16\nbob's connInfo 2") #> ("12", aliceConn, OK)
alice <# ("", bobConn, INFO "bob's connInfo 2")
alice <# ("", bobConn, CON)
bob <# ("", aliceConn, CON)
alice #: ("3", bobConn, "SEND :hi") #> ("3", bobConn, MID 4)
alice <# ("", bobConn, SENT 4)
bob <#= \case ("", c, Msg "hi") -> c == aliceConn; _ -> False
bob #: ("13", aliceConn, "ACK 4") #> ("13", aliceConn, OK)
testRejectContactRequest :: Transport c => TProxy c -> c -> c -> IO ()
testRejectContactRequest _ alice bob = do
("1", "a_contact", Right (INV cReq)) <- alice #: ("1", "a_contact", "NEW CON")
let cReq' = strEncode cReq
bob #: ("11", "alice", "JOIN " <> cReq' <> " 10\nbob's info") #> ("11", "alice", OK)
("", "a_contact", Right (REQ aInvId "bob's info")) <- (alice <#:)
-- RJCT must use correct contact connection
alice #: ("2a", "bob", "RJCT " <> aInvId) #> ("2a", "bob", ERR $ CONN NOT_FOUND)
alice #: ("2b", "a_contact", "RJCT " <> aInvId) #> ("2b", "a_contact", OK)
alice #: ("3", "bob", "ACPT " <> aInvId <> " 12\nalice's info") #> ("3", "bob", ERR $ A.CMD PROHIBITED)
bob #:# "nothing should be delivered to bob"
testSubscription :: Transport c => TProxy c -> c -> c -> c -> IO ()
testSubscription _ alice1 alice2 bob = do
("1", "bob", Right (INV qInfo)) <- alice1 #: ("1", "bob", "NEW")
let qInfo' = serializeSmpQueueInfo qInfo
bob #: ("11", "alice", "JOIN " <> qInfo') #> ("", "alice", CON)
bob #: ("12", "alice", "SEND 5\nhello") =#> \case ("12", "alice", SENT _) -> True; _ -> False
bob #: ("13", "alice", "SEND 11\nhello again") =#> \case ("13", "alice", SENT _) -> True; _ -> False
alice1 <# ("", "bob", CON)
(alice1, "alice") `connect` (bob, "bob")
bob #: ("12", "alice", "SEND 5\nhello") #> ("12", "alice", MID 4)
bob <# ("", "alice", SENT 4)
alice1 <#= \case ("", "bob", Msg "hello") -> True; _ -> False
alice1 #: ("1", "bob", "ACK 4") #> ("1", "bob", OK)
bob #: ("13", "alice", "SEND 11\nhello again") #> ("13", "alice", MID 5)
bob <# ("", "alice", SENT 5)
alice1 <#= \case ("", "bob", Msg "hello again") -> True; _ -> False
alice1 #: ("2", "bob", "ACK 5") #> ("2", "bob", OK)
alice2 #: ("21", "bob", "SUB") #> ("21", "bob", OK)
alice1 <# ("", "bob", END)
bob #: ("14", "alice", "SEND 2\nhi") =#> \case ("14", "alice", SENT _) -> True; _ -> False
bob #: ("14", "alice", "SEND 2\nhi") #> ("14", "alice", MID 6)
bob <# ("", "alice", SENT 6)
alice2 <#= \case ("", "bob", Msg "hi") -> True; _ -> False
alice2 #: ("22", "bob", "ACK 6") #> ("22", "bob", OK)
alice1 #:# "nothing else should be delivered to alice1"
testSubscrNotification :: Transport c => TProxy c -> (ThreadId, ThreadId) -> c -> IO ()
testSubscrNotification _ (server, _) client = do
client #: ("1", "conn1", "NEW") =#> \case ("1", "conn1", INV _) -> True; _ -> False
testSubscrNotification t (server, _) client = do
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", END)
client <# ("", "conn1", DOWN)
withSmpServer (ATransport t) $
client <# ("", "conn1", ERR (SMP AUTH)) -- this new server does not have the queue
samplePublicKey :: ByteString
samplePublicKey = "rsa:MIIBoDANBgkqhkiG9w0BAQEFAAOCAY0AMIIBiAKCAQEAtn1NI2tPoOGSGfad0aUg0tJ0kG2nzrIPGLiz8wb3dQSJC9xkRHyzHhEE8Kmy2cM4q7rNZIlLcm4M7oXOTe7SC4x59bLQG9bteZPKqXu9wk41hNamV25PWQ4zIcIRmZKETVGbwN7jFMpH7wxLdI1zzMArAPKXCDCJ5ctWh4OWDI6OR6AcCtEj+toCI6N6pjxxn5VigJtwiKhxYpoUJSdNM60wVEDCSUrZYBAuDH8pOxPfP+Tm4sokaFDTIG3QJFzOjC+/9nW4MUjAOFll9PCp9kaEFHJ/YmOYKMWNOCCPvLS6lxA83i0UaardkNLNoFS5paWfTlroxRwOC2T6PwO2ywKBgDjtXcSED61zK1seocQMyGRINnlWdhceD669kIHju/f6kAayvYKW3/lbJNXCmyinAccBosO08/0sUxvtuniIo18kfYJE0UmP1ReCjhMP+O+yOmwZJini/QelJk/Pez8IIDDWnY1qYQsN/q7ocjakOYrpGG7mig6JMFpDJtD6istR"
testMsgDeliveryServerRestart :: Transport c => TProxy c -> c -> c -> IO ()
testMsgDeliveryServerRestart t alice bob = do
withServer $ do
connect (alice, "alice") (bob, "bob")
bob #: ("1", "alice", "SEND 2\nhi") #> ("1", "alice", MID 4)
bob <# ("", "alice", SENT 4)
alice <#= \case ("", "bob", Msg "hi") -> True; _ -> False
alice #: ("11", "bob", "ACK 4") #> ("11", "bob", OK)
alice #:# "nothing else delivered before the server is killed"
alice <# ("", "bob", DOWN)
bob #: ("2", "alice", "SEND 11\nhello again") #> ("2", "alice", MID 5)
bob #:# "nothing else delivered before the server is restarted"
alice #:# "nothing else delivered before the server is restarted"
withServer $ do
bob <# ("", "alice", SENT 5)
alice <# ("", "bob", UP)
alice <#= \case ("", "bob", Msg "hello again") -> True; _ -> False
alice #: ("12", "bob", "ACK 5") #> ("12", "bob", OK)
removeFile testStoreLogFile
where
withServer test' = withSmpServerStoreLogOn (ATransport t) testPort2 (const test') `shouldReturn` ()
testServerConnectionAfterError :: forall c. Transport c => TProxy c -> [c] -> IO ()
testServerConnectionAfterError t _ = do
withAgent1 $ \bob -> do
withAgent2 $ \alice -> do
withServer $ do
connect (bob, "bob") (alice, "alice")
bob <# ("", "alice", DOWN)
alice <# ("", "bob", DOWN)
alice #: ("1", "bob", "SEND 5\nhello") #> ("1", "bob", MID 4)
alice #:# "nothing else delivered before the server is restarted"
bob #:# "nothing else delivered before the server is restarted"
withAgent1 $ \bob -> do
withAgent2 $ \alice -> do
bob #: ("1", "alice", "SUB") #> ("1", "alice", ERR (BROKER NETWORK))
alice #: ("1", "bob", "SUB") #> ("1", "bob", ERR (BROKER NETWORK))
withServer $ do
alice <#= \case ("", "bob", cmd) -> cmd == UP || cmd == SENT 4; _ -> False
alice <#= \case ("", "bob", cmd) -> cmd == UP || cmd == SENT 4; _ -> False
bob <# ("", "alice", UP)
bob <#= \case ("", "alice", Msg "hello") -> True; _ -> False
bob #: ("2", "alice", "ACK 4") #> ("2", "alice", OK)
alice #: ("1", "bob", "SEND 11\nhello again") #> ("1", "bob", MID 5)
alice <# ("", "bob", SENT 5)
bob <#= \case ("", "alice", Msg "hello again") -> True; _ -> False
removeFile testStoreLogFile
removeFile testDB
removeFile testDB2
where
withServer test' = withSmpServerStoreLogOn (ATransport t) testPort2 (const test') `shouldReturn` ()
withAgent1 = withAgent agentTestPort testDB
withAgent2 = withAgent agentTestPort2 testDB2
withAgent :: String -> String -> (c -> IO a) -> IO a
withAgent agentPort agentDB = withSmpAgentThreadOn_ (ATransport t) (agentPort, testPort2, agentDB) (pure ()) . const . testSMPAgentClientOn agentPort
testMsgDeliveryAgentRestart :: Transport c => TProxy c -> c -> IO ()
testMsgDeliveryAgentRestart t bob = do
withAgent $ \alice -> do
withServer $ do
connect (bob, "bob") (alice, "alice")
alice #: ("1", "bob", "SEND 5\nhello") #> ("1", "bob", MID 4)
alice <# ("", "bob", SENT 4)
bob <#= \case ("", "alice", Msg "hello") -> True; _ -> False
bob #: ("11", "alice", "ACK 4") #> ("11", "alice", OK)
bob #:# "nothing else delivered before the server is down"
bob <# ("", "alice", DOWN)
alice #: ("2", "bob", "SEND 11\nhello again") #> ("2", "bob", MID 5)
alice #:# "nothing else delivered before the server is restarted"
bob #:# "nothing else delivered before the server is restarted"
withAgent $ \alice -> do
withServer $ do
tPutRaw alice ("3", "bob", "SUB")
alice <#= \case
(corrId, "bob", cmd) ->
(corrId == "3" && cmd == OK)
|| (corrId == "" && cmd == SENT 5)
_ -> False
bob <# ("", "alice", UP)
bob <#= \case ("", "alice", Msg "hello again") -> True; _ -> False
bob #: ("12", "alice", "ACK 5") #> ("12", "alice", OK)
removeFile testStoreLogFile
removeFile testDB
where
withServer test' = withSmpServerStoreLogOn (ATransport t) testPort2 (const test') `shouldReturn` ()
withAgent = withSmpAgentThreadOn_ (ATransport t) (agentTestPort, testPort, testDB) (pure ()) . const . testSMPAgentClientOn agentTestPort
testConcurrentMsgDelivery :: Transport c => TProxy c -> c -> c -> IO ()
testConcurrentMsgDelivery _ alice bob = do
connect (alice, "alice") (bob, "bob")
("1", "bob2", Right (INV cReq)) <- alice #: ("1", "bob2", "NEW INV")
let cReq' = strEncode cReq
bob #: ("11", "alice2", "JOIN " <> cReq' <> " 14\nbob's connInfo") #> ("11", "alice2", OK)
("", "bob2", Right (CONF _confId "bob's connInfo")) <- (alice <#:)
-- below commands would be needed to accept bob's connection, but alice does not
-- alice #: ("2", "bob", "LET " <> _confId <> " 16\nalice's connInfo") #> ("2", "bob", OK)
-- bob <# ("", "alice", INFO "alice's connInfo")
-- bob <# ("", "alice", CON)
-- alice <# ("", "bob", CON)
-- the first connection should not be blocked by the second one
sendMessage (alice, "alice") (bob, "bob") "hello"
-- alice #: ("2", "bob", "SEND :hello") #> ("2", "bob", MID 1)
-- alice <# ("", "bob", SENT 1)
-- bob <#= \case ("", "alice", Msg "hello") -> True; _ -> False
-- bob #: ("12", "alice", "ACK 1") #> ("12", "alice", OK)
bob #: ("14", "alice", "SEND 9\nhello too") #> ("14", "alice", MID 5)
bob <# ("", "alice", SENT 5)
-- if delivery is blocked it won't go further
alice <#= \case ("", "bob", Msg "hello too") -> True; _ -> False
alice #: ("3", "bob", "ACK 5") #> ("3", "bob", OK)
testMsgDeliveryQuotaExceeded :: Transport c => TProxy c -> c -> c -> IO ()
testMsgDeliveryQuotaExceeded _ alice bob = do
connect (alice, "alice") (bob, "bob")
connect (alice, "alice2") (bob, "bob2")
forM_ [1 .. 4 :: Int] $ \i -> do
let corrId = bshow i
msg = "message " <> bshow i
(_, "bob", Right (MID mId)) <- alice #: (corrId, "bob", "SEND :" <> msg)
alice <#= \case ("", "bob", SENT m) -> m == mId; _ -> False
(_, "bob", Right (MID _)) <- alice #: ("5", "bob", "SEND :over quota")
alice #: ("1", "bob2", "SEND :hello") #> ("1", "bob2", MID 4)
-- if delivery is blocked it won't go further
alice <# ("", "bob2", SENT 4)
connect :: forall c. Transport c => (c, ByteString) -> (c, ByteString) -> IO ()
connect (h1, name1) (h2, name2) = do
("c1", _, Right (INV cReq)) <- h1 #: ("c1", name2, "NEW INV")
let cReq' = strEncode cReq
h2 #: ("c2", name1, "JOIN " <> cReq' <> " 5\ninfo2") #> ("c2", name1, OK)
("", _, Right (CONF connId "info2")) <- (h1 <#:)
h1 #: ("c3", name2, "LET " <> connId <> " 5\ninfo1") #> ("c3", name2, OK)
h2 <# ("", name1, INFO "info1")
h2 <# ("", name1, CON)
h1 <# ("", name2, CON)
sendMessage :: Transport c => (c, ConnId) -> (c, ConnId) -> ByteString -> IO ()
sendMessage (h1, name1) (h2, name2) msg = do
("m1", name2', Right (MID mId)) <- h1 #: ("m1", name2, "SEND :" <> msg)
name2' `shouldBe` name2
h1 <#= \case ("", n, SENT m) -> n == name2 && m == mId; _ -> False
("", name1', Right (MSG MsgMeta {recipient = (msgId', _)} msg')) <- (h2 <#:)
name1' `shouldBe` name1
msg' `shouldBe` msg
h2 #: ("m2", name1, "ACK " <> bshow msgId') =#> \case ("m2", n, OK) -> n == name1; _ -> False
-- connect' :: forall c. Transport c => c -> c -> IO (ByteString, ByteString)
-- connect' h1 h2 = do
-- ("c1", conn2, Right (INV cReq)) <- h1 #: ("c1", "", "NEW INV")
-- let cReq' = strEncode cReq
-- ("c2", conn1, Right OK) <- h2 #: ("c2", "", "JOIN " <> cReq' <> " 5\ninfo2")
-- ("", _, Right (REQ connId "info2")) <- (h1 <#:)
-- h1 #: ("c3", conn2, "ACPT " <> connId <> " 5\ninfo1") =#> \case ("c3", c, OK) -> c == conn2; _ -> False
-- h2 <# ("", conn1, INFO "info1")
-- h2 <# ("", conn1, CON)
-- h1 <# ("", conn2, CON)
-- pure (conn1, conn2)
sampleDhKey :: ByteString
sampleDhKey = "MCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o="
syntaxTests :: forall c. Transport c => TProxy c -> Spec
syntaxTests t = do
it "unknown command" $ ("1", "5678", "HELLO") >#> ("1", "5678", "ERR CMD SYNTAX")
describe "NEW" 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
xit "without parameters" $ ("211", "", "NEW") >#>= \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")
describe "NEW" $ do
describe "valid" $ do
-- TODO: add tests with defined connection id
it "with correct parameter" $ ("211", "", "NEW INV") >#>= \case ("211", _, "INV" : _) -> True; _ -> False
describe "invalid" $ do
-- TODO: add tests with defined connection id
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
describe "JOIN" $ do
describe "valid" $ do
it "using same server as in invitation" $
("311", "", "JOIN smp::localhost:5000::1234::" <> samplePublicKey) >#> ("311", "", "ERR SMP AUTH")
describe "invalid" do
-- TODO: JOIN is not merged yet - to be added
( "311",
"a",
"JOIN https://simpex.chat/invitation#/?smp=smp%3A%2F%2F"
<> urlEncode True "LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI="
<> "%40localhost%3A5001%2F3456-w%3D%3D%23"
<> urlEncode True sampleDhKey
<> "&v=1"
<> "&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D"
<> " 14\nbob's connInfo"
)
>#> ("311", "a", "ERR SMP AUTH")
describe "invalid" $ do
it "no parameters" $ ("321", "", "JOIN") >#> ("321", "", "ERR CMD SYNTAX")
where
-- simple test for one command with the expected response
@@ -149,4 +484,4 @@ syntaxTests t = do
-- simple test for one command with a predicate for the expected response
(>#>=) :: ARawTransmission -> ((ByteString, ByteString, [ByteString]) -> Bool) -> Expectation
command >#>= p = smpAgentTest t command >>= (`shouldSatisfy` p . \(cId, cAlias, cmd) -> (cId, cAlias, B.words cmd))
command >#>= p = smpAgentTest t command >>= (`shouldSatisfy` p . \(cId, connId, cmd) -> (cId, connId, B.words cmd))
+139
View File
@@ -0,0 +1,139 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
module AgentTests.ConnectionRequestTests where
import Data.ByteString (ByteString)
import Network.HTTP.Types (urlEncode)
import Simplex.Messaging.Agent.Protocol
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.Ratchet
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol (smpClientVRange)
import Simplex.Messaging.Version
import Test.Hspec
uri :: String
uri = "smp.simplex.im"
srv :: SMPServer
srv =
SMPServer
{ host = "smp.simplex.im",
port = "5223",
keyHash = C.KeyHash "\215m\248\251"
}
queue :: SMPQueueUri
queue =
SMPQueueUri
{ smpServer = srv,
senderId = "\223\142z\251",
clientVRange = smpClientVRange,
dhPublicKey = testDhKey
}
testDhKey :: C.PublicKeyX25519
testDhKey = "MCowBQYDK2VuAyEAjiswwI3O/NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o="
testDhKeyStr :: ByteString
testDhKeyStr = strEncode testDhKey
testDhKeyStrUri :: ByteString
testDhKeyStrUri = urlEncode True testDhKeyStr
connReqData :: ConnReqUriData
connReqData =
ConnReqUriData
{ crScheme = simplexChat,
crAgentVRange = smpAgentVRange,
crSmpQueues = [queue]
}
testDhPubKey :: C.PublicKeyX448
testDhPubKey = "MEIwBQYDK2VvAzkAmKuSYeQ/m0SixPDS8Wq8VBaTS1cW+Lp0n0h4Diu+kUpR+qXx4SDJ32YGEFoGFGSbGPry5Ychr6U="
testE2ERatchetParams :: E2ERatchetParamsUri 'C.X448
testE2ERatchetParams = E2ERatchetParamsUri e2eEncryptVRange testDhPubKey testDhPubKey
testE2ERatchetParams13 :: E2ERatchetParamsUri 'C.X448
testE2ERatchetParams13 = E2ERatchetParamsUri (mkVersionRange 1 3) testDhPubKey testDhPubKey
connectionRequest :: AConnectionRequestUri
connectionRequest =
ACR SCMInvitation $
CRInvitationUri connReqData testE2ERatchetParams
connectionRequest12 :: AConnectionRequestUri
connectionRequest12 =
ACR SCMInvitation $
CRInvitationUri
connReqData {crAgentVRange = mkVersionRange 1 2, crSmpQueues = [queue, queue]}
testE2ERatchetParams13
connectionRequestTests :: Spec
connectionRequestTests =
describe "connection request parsing / serializing" $ do
it "should serialize SMP queue URIs" $ do
strEncode (queue :: SMPQueueUri) {smpServer = srv {port = ""}}
`shouldBe` "smp://1234-w==@smp.simplex.im/3456-w==#" <> testDhKeyStr
strEncode queue {clientVRange = mkVersionRange 1 2}
`shouldBe` "smp://1234-w==@smp.simplex.im:5223/3456-w==#" <> testDhKeyStr
it "should parse SMP queue URIs" $ do
strDecode ("smp://1234-w==@smp.simplex.im/3456-w==#/?v=1&dh=" <> testDhKeyStr)
`shouldBe` Right (queue :: SMPQueueUri) {smpServer = srv {port = ""}}
strDecode ("smp://1234-w==@smp.simplex.im/3456-w==#" <> testDhKeyStr)
`shouldBe` Right (queue :: SMPQueueUri) {smpServer = srv {port = ""}}
strDecode ("smp://1234-w==@smp.simplex.im:5223/3456-w==#" <> testDhKeyStr)
`shouldBe` Right queue
strDecode ("smp://1234-w==@smp.simplex.im:5223/3456-w==#" <> testDhKeyStr <> "/?v=1&extra_param=abc")
`shouldBe` Right queue
strDecode ("smp://1234-w==@smp.simplex.im:5223/3456-w==#/?extra_param=abc&v=1-2&dh=" <> testDhKeyStr)
`shouldBe` Right queue {clientVRange = mkVersionRange 1 2}
strDecode ("smp://1234-w==@smp.simplex.im:5223/3456-w==#" <> testDhKeyStr <> "/?v=1-2&extra_param=abc")
`shouldBe` Right queue {clientVRange = mkVersionRange 1 2}
it "should serialize connection requests" $ do
strEncode connectionRequest
`shouldBe` "https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23"
<> testDhKeyStrUri
<> "&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D"
strEncode connectionRequest12
`shouldBe` "https://simplex.chat/invitation#/?v=1-2&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23"
<> testDhKeyStrUri
<> "%2Csmp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23"
<> testDhKeyStrUri
<> "&e2e=v%3D1-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D"
it "should parse connection requests" $ do
strDecode
( "https://simplex.chat/invitation#/?smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23"
<> testDhKeyStrUri
<> "&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D"
<> "&v=1"
)
`shouldBe` Right connectionRequest
strDecode
( "https://simplex.chat/invitation#/?smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1%26dh%3D"
<> testDhKeyStrUri
<> "&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D"
<> "&v=1"
)
`shouldBe` Right connectionRequest
strDecode
( "https://simplex.chat/invitation#/?smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1%26dh%3D"
<> testDhKeyStrUri
<> "&e2e=v%3D1-1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D"
<> "&v=1-1"
)
`shouldBe` Right connectionRequest
strDecode
( "https://simplex.chat/invitation#/?smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1%26extra_param%3Dabc%26dh%3D"
<> testDhKeyStrUri
<> "%2Csmp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1%26dh%3D"
<> testDhKeyStrUri
<> "&e2e=extra_key%3Dnew%26v%3D1-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D"
<> "&some_new_param=abc"
<> "&v=1-2"
)
`shouldBe` Right connectionRequest12
+232
View File
@@ -0,0 +1,232 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-}
module AgentTests.DoubleRatchetTests where
import Control.Concurrent.STM
import Control.Monad.Except
import Data.Aeson (FromJSON, ToJSON)
import qualified Data.Aeson as J
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import qualified Data.Map.Strict as M
import Simplex.Messaging.Crypto (Algorithm (..), AlgorithmI, CryptoError, DhAlgorithm)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.Ratchet
import Simplex.Messaging.Encoding
import Simplex.Messaging.Parsers (parseAll)
import Simplex.Messaging.Util ((<$$>))
import Test.Hspec
doubleRatchetTests :: Spec
doubleRatchetTests = do
describe "double-ratchet encryption/decryption" $ do
it "should serialize and parse message header" testMessageHeader
it "should encrypt and decrypt messages" $ do
withRatchets @X25519 testEncryptDecrypt
withRatchets @X448 testEncryptDecrypt
it "should encrypt and decrypt skipped messages" $ do
withRatchets @X25519 testSkippedMessages
withRatchets @X448 testSkippedMessages
it "should encrypt and decrypt many messages" $ do
withRatchets @X25519 testManyMessages
it "should allow skipped after ratchet advance" $ do
withRatchets @X25519 testSkippedAfterRatchetAdvance
it "should encode/decode ratchet as JSON" $ do
testKeyJSON C.SX25519
testKeyJSON C.SX448
testRatchetJSON C.SX25519
testRatchetJSON C.SX448
it "should agree the same ratchet parameters" $ do
testX3dh C.SX25519
testX3dh C.SX448
paddedMsgLen :: Int
paddedMsgLen = 100
fullMsgLen :: Int
fullMsgLen = 1 + fullHeaderLen + C.authTagSize + paddedMsgLen
testMessageHeader :: Expectation
testMessageHeader = do
(k, _) <- C.generateKeyPair' @X25519
let hdr = MsgHeader {msgMaxVersion = e2eEncryptVersion, msgDHRs = k, msgPN = 0, msgNs = 0}
parseAll (smpP @(MsgHeader 'X25519)) (smpEncode hdr) `shouldBe` Right hdr
pattern Decrypted :: ByteString -> Either CryptoError (Either CryptoError ByteString)
pattern Decrypted msg <- Right (Right msg)
type TestRatchets a = (AlgorithmI a, DhAlgorithm a) => TVar (Ratchet a, SkippedMsgKeys) -> TVar (Ratchet a, SkippedMsgKeys) -> IO ()
testEncryptDecrypt :: TestRatchets a
testEncryptDecrypt alice bob = do
(bob, "hello alice") #> alice
(alice, "hello bob") #> bob
Right b1 <- encrypt bob "how are you, alice?"
Right b2 <- encrypt bob "are you there?"
Right b3 <- encrypt bob "hey?"
Right a1 <- encrypt alice "how are you, bob?"
Right a2 <- encrypt alice "are you there?"
Right a3 <- encrypt alice "hey?"
Decrypted "how are you, alice?" <- decrypt alice b1
Decrypted "are you there?" <- decrypt alice b2
Decrypted "hey?" <- decrypt alice b3
Decrypted "how are you, bob?" <- decrypt bob a1
Decrypted "are you there?" <- decrypt bob a2
Decrypted "hey?" <- decrypt bob a3
(bob, "I'm here, all good") #> alice
(alice, "I'm here too, same") #> bob
pure ()
testSkippedMessages :: TestRatchets a
testSkippedMessages alice bob = do
Right msg1 <- encrypt bob "hello alice"
Right msg2 <- encrypt bob "hello there again"
Right msg3 <- encrypt bob "are you there?"
Decrypted "are you there?" <- decrypt alice msg3
Right (Left C.CERatchetDuplicateMessage) <- decrypt alice msg3
Decrypted "hello there again" <- decrypt alice msg2
Decrypted "hello alice" <- decrypt alice msg1
pure ()
testManyMessages :: TestRatchets a
testManyMessages alice bob = do
(bob, "b1") #> alice
(bob, "b2") #> alice
(bob, "b3") #> alice
(bob, "b4") #> alice
(alice, "a5") #> bob
(alice, "a6") #> bob
(alice, "a7") #> bob
(bob, "b8") #> alice
(alice, "a9") #> bob
(alice, "a10") #> bob
(bob, "b11") #> alice
(bob, "b12") #> alice
(alice, "a14") #> bob
(bob, "b15") #> alice
(bob, "b16") #> alice
testSkippedAfterRatchetAdvance :: TestRatchets a
testSkippedAfterRatchetAdvance alice bob = do
(bob, "b1") #> alice
Right b2 <- encrypt bob "b2"
Right b3 <- encrypt bob "b3"
Right b4 <- encrypt bob "b4"
(alice, "a5") #> bob
Right b5 <- encrypt bob "b5"
Right b6 <- encrypt bob "b6"
(bob, "b7") #> alice
Right b8 <- encrypt bob "b8"
Right b9 <- encrypt bob "b9"
(alice, "a10") #> bob
Right b11 <- encrypt bob "b11"
Right b12 <- encrypt bob "b12"
(alice, "a14") #> bob
Decrypted "b12" <- decrypt alice b12
Decrypted "b2" <- decrypt alice b2
-- fails on duplicate message
Left C.CERatchetHeader <- decrypt alice b2
(alice, "a15") #> bob
Right a16 <- encrypt bob "a16"
Right a17 <- encrypt bob "a17"
Decrypted "b8" <- decrypt alice b8
Decrypted "b3" <- decrypt alice b3
Decrypted "b4" <- decrypt alice b4
Decrypted "b5" <- decrypt alice b5
Decrypted "b6" <- decrypt alice b6
(alice, "a18") #> bob
Decrypted "a16" <- decrypt alice a16
Decrypted "a17" <- decrypt alice a17
Decrypted "b9" <- decrypt alice b9
Decrypted "b11" <- decrypt alice b11
pure ()
testKeyJSON :: forall a. AlgorithmI a => C.SAlgorithm a -> IO ()
testKeyJSON _ = do
(k, pk) <- C.generateKeyPair' @a
testEncodeDecode k
testEncodeDecode pk
testRatchetJSON :: forall a. (AlgorithmI a, DhAlgorithm a) => C.SAlgorithm a -> IO ()
testRatchetJSON _ = do
(alice, bob) <- initRatchets @a
testEncodeDecode alice
testEncodeDecode bob
testEncodeDecode :: (Eq a, Show a, ToJSON a, FromJSON a) => a -> Expectation
testEncodeDecode x = do
let j = J.encode x
x' = J.eitherDecode' j
x' `shouldBe` Right x
testX3dh :: forall a. (AlgorithmI a, DhAlgorithm a) => C.SAlgorithm a -> IO ()
testX3dh _ = do
(pkBob1, pkBob2, e2eBob) <- generateE2EParams @a e2eEncryptVersion
(pkAlice1, pkAlice2, e2eAlice) <- generateE2EParams @a e2eEncryptVersion
let paramsBob = x3dhSnd pkBob1 pkBob2 e2eAlice
paramsAlice = x3dhRcv pkAlice1 pkAlice2 e2eBob
paramsAlice `shouldBe` paramsBob
(#>) :: (AlgorithmI a, DhAlgorithm a) => (TVar (Ratchet a, SkippedMsgKeys), ByteString) -> TVar (Ratchet a, SkippedMsgKeys) -> Expectation
(alice, msg) #> bob = do
Right msg' <- encrypt alice msg
Decrypted msg'' <- decrypt bob msg'
msg'' `shouldBe` msg
withRatchets :: forall a. (AlgorithmI a, DhAlgorithm a) => (TVar (Ratchet a, SkippedMsgKeys) -> TVar (Ratchet a, SkippedMsgKeys) -> IO ()) -> Expectation
withRatchets test = do
(a, b) <- initRatchets @a
alice <- newTVarIO (a, M.empty)
bob <- newTVarIO (b, M.empty)
test alice bob `shouldReturn` ()
initRatchets :: (AlgorithmI a, DhAlgorithm a) => IO (Ratchet a, Ratchet a)
initRatchets = do
(pkBob1, pkBob2, e2eBob) <- generateE2EParams e2eEncryptVersion
(pkAlice1, pkAlice2, e2eAlice) <- generateE2EParams e2eEncryptVersion
let paramsBob = x3dhSnd pkBob1 pkBob2 e2eAlice
paramsAlice = x3dhRcv pkAlice1 pkAlice2 e2eBob
(_, pkBob3) <- C.generateKeyPair'
let bob = initSndRatchet (C.publicKey pkAlice2) pkBob3 paramsBob
alice = initRcvRatchet pkAlice2 paramsAlice
pure (alice, bob)
encrypt_ :: AlgorithmI a => (Ratchet a, SkippedMsgKeys) -> ByteString -> IO (Either CryptoError (ByteString, Ratchet a, SkippedMsgDiff))
encrypt_ (rc, _) msg =
runExceptT (rcEncrypt rc paddedMsgLen msg)
>>= either (pure . Left) checkLength
where
checkLength (msg', rc') = do
B.length msg' `shouldBe` fullMsgLen
pure $ Right (msg', rc', SMDNoChange)
decrypt_ :: (AlgorithmI a, DhAlgorithm a) => (Ratchet a, SkippedMsgKeys) -> ByteString -> IO (Either CryptoError (Either CryptoError ByteString, Ratchet a, SkippedMsgDiff))
decrypt_ (rc, smks) msg = runExceptT $ rcDecrypt rc smks msg
encrypt :: AlgorithmI a => TVar (Ratchet a, SkippedMsgKeys) -> ByteString -> IO (Either CryptoError ByteString)
encrypt = withTVar encrypt_
decrypt :: (AlgorithmI a, DhAlgorithm a) => TVar (Ratchet a, SkippedMsgKeys) -> ByteString -> IO (Either CryptoError (Either CryptoError ByteString))
decrypt = withTVar decrypt_
withTVar ::
AlgorithmI a =>
((Ratchet a, SkippedMsgKeys) -> ByteString -> IO (Either e (r, Ratchet a, SkippedMsgDiff))) ->
TVar (Ratchet a, SkippedMsgKeys) ->
ByteString ->
IO (Either e r)
withTVar op rcVar msg =
readTVarIO rcVar
>>= (\(rc, smks) -> applyDiff smks <$$> (testEncodeDecode rc >> op (rc, smks) msg))
>>= \case
Right (res, rc', smks') -> atomically (writeTVar rcVar (rc', smks')) >> pure (Right res)
Left e -> pure $ Left e
where
applyDiff smks (res, rc', smDiff) = (res, rc', applySMDiff smks smDiff)
+171
View File
@@ -0,0 +1,171 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-}
module AgentTests.FunctionalAPITests (functionalAPITests) where
import Control.Monad.Except (ExceptT, runExceptT)
import Control.Monad.IO.Unlift
import SMPAgentClient
import SMPClient (withSmpServer)
import Simplex.Messaging.Agent
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..))
import Simplex.Messaging.Agent.Protocol
import Simplex.Messaging.Protocol (ErrorType (..), MsgBody)
import Simplex.Messaging.Transport (ATransport (..))
import System.Timeout
import Test.Hspec
import UnliftIO.STM
(##>) :: MonadIO m => m (ATransmission 'Agent) -> ATransmission 'Agent -> m ()
a ##> t = a >>= \t' -> liftIO (t' `shouldBe` t)
(=##>) :: MonadIO m => m (ATransmission 'Agent) -> (ATransmission 'Agent -> Bool) -> m ()
a =##> p = a >>= \t -> liftIO (t `shouldSatisfy` p)
get :: MonadIO m => AgentClient -> m (ATransmission 'Agent)
get c = atomically (readTBQueue $ subQ c)
pattern Msg :: MsgBody -> ACommand 'Agent
pattern Msg msgBody <- MSG MsgMeta {integrity = MsgOk} msgBody
functionalAPITests :: ATransport -> Spec
functionalAPITests t = do
describe "Establishing duplex connection" $
it "should connect via one server using SMP agent clients" $
withSmpServer t testAgentClient
describe "Establishing connection asynchronously" $ do
it "should connect with initiating client going offline" $
withSmpServer t testAsyncInitiatingOffline
it "should connect with joining client going offline before its queue activation" $
withSmpServer t testAsyncJoiningOfflineBeforeActivation
it "should connect with both clients going offline" $
withSmpServer t testAsyncBothOffline
it "should notify after HELLO timeout" $
withSmpServer t testAsyncHelloTimeout
testAgentClient :: IO ()
testAgentClient = do
alice <- getSMPAgentClient cfg
bob <- getSMPAgentClient cfg {dbFile = testDB2}
Right () <- runExceptT $ do
(bobId, qInfo) <- createConnection alice SCMInvitation
aliceId <- joinConnection bob qInfo "bob's connInfo"
("", _, CONF confId "bob's connInfo") <- get alice
allowConnection alice bobId confId "alice's connInfo"
get alice ##> ("", bobId, CON)
get bob ##> ("", aliceId, INFO "alice's connInfo")
get bob ##> ("", aliceId, CON)
-- message IDs 1 to 3 get assigned to control messages, so first MSG is assigned ID 4
4 <- sendMessage alice bobId "hello"
get alice ##> ("", bobId, SENT 4)
5 <- sendMessage alice bobId "how are you?"
get alice ##> ("", bobId, SENT 5)
get bob =##> \case ("", c, Msg "hello") -> c == aliceId; _ -> False
ackMessage bob aliceId 4
get bob =##> \case ("", c, Msg "how are you?") -> c == aliceId; _ -> False
ackMessage bob aliceId 5
6 <- sendMessage bob aliceId "hello too"
get bob ##> ("", aliceId, SENT 6)
7 <- sendMessage bob aliceId "message 1"
get bob ##> ("", aliceId, SENT 7)
get alice =##> \case ("", c, Msg "hello too") -> c == bobId; _ -> False
ackMessage alice bobId 6
get alice =##> \case ("", c, Msg "message 1") -> c == bobId; _ -> False
ackMessage alice bobId 7
suspendConnection alice bobId
8 <- sendMessage bob aliceId "message 2"
get bob ##> ("", aliceId, MERR 8 (SMP AUTH))
deleteConnection alice bobId
liftIO $ noMessages alice "nothing else should be delivered to alice"
pure ()
where
noMessages :: AgentClient -> String -> Expectation
noMessages c err = tryGet `shouldReturn` ()
where
tryGet =
10000 `timeout` get c >>= \case
Just _ -> error err
_ -> return ()
testAsyncInitiatingOffline :: IO ()
testAsyncInitiatingOffline = do
alice <- getSMPAgentClient cfg
bob <- getSMPAgentClient cfg {dbFile = testDB2}
Right () <- runExceptT $ do
(bobId, cReq) <- createConnection alice SCMInvitation
disconnectAgentClient alice
aliceId <- joinConnection bob cReq "bob's connInfo"
alice' <- liftIO $ getSMPAgentClient cfg
subscribeConnection alice' bobId
("", _, CONF confId "bob's connInfo") <- get alice'
allowConnection alice' bobId confId "alice's connInfo"
get alice' ##> ("", bobId, CON)
get bob ##> ("", aliceId, INFO "alice's connInfo")
get bob ##> ("", aliceId, CON)
exchangeGreetings alice' bobId bob aliceId
pure ()
testAsyncJoiningOfflineBeforeActivation :: IO ()
testAsyncJoiningOfflineBeforeActivation = do
alice <- getSMPAgentClient cfg
bob <- getSMPAgentClient cfg {dbFile = testDB2}
Right () <- runExceptT $ do
(bobId, qInfo) <- createConnection alice SCMInvitation
aliceId <- joinConnection bob qInfo "bob's connInfo"
disconnectAgentClient bob
("", _, CONF confId "bob's connInfo") <- get alice
allowConnection alice bobId confId "alice's connInfo"
bob' <- liftIO $ getSMPAgentClient cfg {dbFile = testDB2}
subscribeConnection bob' aliceId
get alice ##> ("", bobId, CON)
get bob' ##> ("", aliceId, INFO "alice's connInfo")
get bob' ##> ("", aliceId, CON)
exchangeGreetings alice bobId bob' aliceId
pure ()
testAsyncBothOffline :: IO ()
testAsyncBothOffline = do
alice <- getSMPAgentClient cfg
bob <- getSMPAgentClient cfg {dbFile = testDB2}
Right () <- runExceptT $ do
(bobId, cReq) <- createConnection alice SCMInvitation
disconnectAgentClient alice
aliceId <- joinConnection bob cReq "bob's connInfo"
disconnectAgentClient bob
alice' <- liftIO $ getSMPAgentClient cfg
subscribeConnection alice' bobId
("", _, CONF confId "bob's connInfo") <- get alice'
allowConnection alice' bobId confId "alice's connInfo"
bob' <- liftIO $ getSMPAgentClient cfg {dbFile = testDB2}
subscribeConnection bob' aliceId
get alice' ##> ("", bobId, CON)
get bob' ##> ("", aliceId, INFO "alice's connInfo")
get bob' ##> ("", aliceId, CON)
exchangeGreetings alice' bobId bob' aliceId
pure ()
testAsyncHelloTimeout :: IO ()
testAsyncHelloTimeout = do
alice <- getSMPAgentClient cfg
bob <- getSMPAgentClient cfg {dbFile = testDB2, helloTimeout = 1}
Right () <- runExceptT $ do
(_, cReq) <- createConnection alice SCMInvitation
disconnectAgentClient alice
aliceId <- joinConnection bob cReq "bob's connInfo"
get bob ##> ("", aliceId, ERR $ CONN NOT_ACCEPTED)
pure ()
exchangeGreetings :: AgentClient -> ConnId -> AgentClient -> ConnId -> ExceptT AgentErrorType IO ()
exchangeGreetings alice bobId bob aliceId = do
4 <- sendMessage alice bobId "hello"
get alice ##> ("", bobId, SENT 4)
get bob =##> \case ("", c, Msg "hello") -> c == aliceId; _ -> False
ackMessage bob aliceId 4
5 <- sendMessage bob aliceId "hello too"
get bob ##> ("", aliceId, SENT 5)
get alice =##> \case ("", c, Msg "hello too") -> c == bobId; _ -> False
ackMessage alice bobId 5
+189 -145
View File
@@ -1,5 +1,6 @@
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
@@ -8,9 +9,10 @@
module AgentTests.SQLiteTests (storeTests) where
import Control.Concurrent.Async (concurrently_)
import Control.Concurrent.STM
import Control.Monad (replicateM_)
import Control.Monad.Except (ExceptT, runExceptT)
import qualified Crypto.PubKey.RSA as R
import Crypto.Random (drgNew)
import Data.ByteString.Char8 (ByteString)
import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8)
@@ -19,11 +21,13 @@ import Data.Word (Word32)
import qualified Database.SQLite.Simple as DB
import Database.SQLite.Simple.QQ (sql)
import SMPClient (testKeyHash)
import Simplex.Messaging.Agent.Client ()
import Simplex.Messaging.Agent.Protocol
import Simplex.Messaging.Agent.Store
import Simplex.Messaging.Agent.Store.SQLite
import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations
import qualified Simplex.Messaging.Crypto as C
import System.Random (Random (randomIO))
import System.Random
import Test.Hspec
import UnliftIO.Directory (removeFile)
@@ -39,7 +43,7 @@ withStore2 = before connect2 . after (removeStore . fst)
connect2 :: IO (SQLiteStore, SQLiteStore)
connect2 = do
s1 <- createStore
s2 <- connectSQLiteStore $ dbFilePath s1
s2 <- connectSQLiteStore (dbFilePath s1) 4
pure (s1, s2)
createStore :: IO SQLiteStore
@@ -47,12 +51,15 @@ createStore = do
-- Randomize DB file name to avoid SQLite IO errors supposedly caused by asynchronous
-- IO operations on multiple similarly named files; error seems to be environment specific
r <- randomIO :: IO Word32
createSQLiteStore $ testDB <> show r
createSQLiteStore (testDB <> show r) 4 Migrations.app True
removeStore :: SQLiteStore -> IO ()
removeStore store = do
DB.close $ dbConn store
close store
removeFile $ dbFilePath store
where
close :: SQLiteStore -> IO ()
close st = mapM_ DB.close =<< atomically (flushTBQueue $ dbConnPool st)
returnsResult :: (Eq a, Eq e, Show a, Show e) => ExceptT e IO a -> a -> Expectation
action `returnsResult` r = runExceptT action `shouldReturn` Right r
@@ -63,40 +70,39 @@ action `throwsError` e = runExceptT action `shouldReturn` Left e
-- TODO add null port tests
storeTests :: Spec
storeTests = do
withStore2 do
withStore2 $ do
describe "stress test" testConcurrentWrites
withStore do
describe "store setup" do
withStore $ do
describe "store setup" $ do
testCompiledThreadsafe
testForeignKeysEnabled
describe "store methods" do
describe "Queue and Connection management" do
describe "createRcvConn" do
describe "store methods" $ do
describe "Queue and Connection management" $ do
describe "createRcvConn" $ do
testCreateRcvConn
testCreateRcvConnRandomId
testCreateRcvConnDuplicate
describe "createSndConn" do
describe "createSndConn" $ do
testCreateSndConn
testCreateSndConnRandomID
testCreateSndConnDuplicate
describe "getAllConnAliases" testGetAllConnAliases
describe "getRcvConn" testGetRcvConn
describe "deleteConn" do
describe "deleteConn" $ do
testDeleteRcvConn
testDeleteSndConn
testDeleteDuplexConn
describe "upgradeRcvConnToDuplex" do
describe "upgradeRcvConnToDuplex" $ do
testUpgradeRcvConnToDuplex
describe "upgradeSndConnToDuplex" do
describe "upgradeSndConnToDuplex" $ do
testUpgradeSndConnToDuplex
describe "set Queue status" do
describe "setRcvQueueStatus" do
describe "set Queue status" $ do
describe "setRcvQueueStatus" $ do
testSetRcvQueueStatus
testSetRcvQueueStatusNoQueue
describe "setSndQueueStatus" do
describe "setSndQueueStatus" $ do
testSetSndQueueStatus
testSetSndQueueStatusNoQueue
testSetQueueStatusDuplex
describe "Msg management" do
describe "create Msg" do
describe "Msg management" $ do
describe "create Msg" $ do
testCreateRcvMsg
testCreateSndMsg
testCreateRcvAndSndMsgs
@@ -104,121 +110,157 @@ storeTests = do
testConcurrentWrites :: SpecWith (SQLiteStore, SQLiteStore)
testConcurrentWrites =
it "should complete multiple concurrent write transactions w/t sqlite busy errors" $ \(s1, s2) -> do
_ <- runExceptT $ createRcvConn s1 rcvQueue1
concurrently_ (runTest s1) (runTest s2)
g <- newTVarIO =<< drgNew
_ <- runExceptT $ createRcvConn s1 g cData1 rcvQueue1 SCMInvitation
let ConnData {connId} = cData1
concurrently_ (runTest s1 connId) (runTest s2 connId)
where
runTest :: SQLiteStore -> IO (Either StoreError ())
runTest store = runExceptT . replicateM_ 100 $ do
(internalId, internalRcvId, _, _) <- updateRcvIds store rcvQueue1
runTest :: SQLiteStore -> ConnId -> IO (Either StoreError ())
runTest store connId = runExceptT . replicateM_ 100 $ do
(internalId, internalRcvId, _, _) <- updateRcvIds store connId
let rcvMsgData = mkRcvMsgData internalId internalRcvId 0 "0" "hash_dummy"
createRcvMsg store rcvQueue1 rcvMsgData
createRcvMsg store connId rcvMsgData
testCompiledThreadsafe :: SpecWith SQLiteStore
testCompiledThreadsafe =
it "compiled sqlite library should be threadsafe" $ \store -> do
compileOptions <- DB.query_ (dbConn store) "pragma COMPILE_OPTIONS;" :: IO [[T.Text]]
it "compiled sqlite library should be threadsafe" . withStoreConnection $ \db -> do
compileOptions <- DB.query_ db "pragma COMPILE_OPTIONS;" :: IO [[T.Text]]
compileOptions `shouldNotContain` [["THREADSAFE=0"]]
withStoreConnection :: (DB.Connection -> IO a) -> SQLiteStore -> IO a
withStoreConnection = flip withConnection
testForeignKeysEnabled :: SpecWith SQLiteStore
testForeignKeysEnabled =
it "foreign keys should be enabled" $ \store -> do
it "foreign keys should be enabled" . withStoreConnection $ \db -> do
let inconsistentQuery =
[sql|
INSERT INTO connections
(conn_alias, rcv_host, rcv_port, rcv_id, snd_host, snd_port, snd_id)
INSERT INTO snd_queues
( host, port, snd_id, conn_id, snd_private_key, e2e_dh_secret, status)
VALUES
("conn1", "smp.simplex.im", "5223", "1234", "smp.simplex.im", "5223", "2345");
('smp.simplex.im', '5223', '1234', '2345', x'', x'', 'new');
|]
DB.execute_ (dbConn store) inconsistentQuery
DB.execute_ db inconsistentQuery
`shouldThrow` (\e -> DB.sqlError e == DB.ErrorConstraint)
cData1 :: ConnData
cData1 = ConnData {connId = "conn1"}
testPrivateSignKey :: C.APrivateSignKey
testPrivateSignKey = C.APrivateSignKey C.SEd25519 "MC4CAQAwBQYDK2VwBCIEIDfEfevydXXfKajz3sRkcQ7RPvfWUPoq6pu1TYHV1DEe"
testPrivDhKey :: C.PrivateKeyX25519
testPrivDhKey = "MC4CAQAwBQYDK2VuBCIEINCzbVFaCiYHoYncxNY8tSIfn0pXcIAhLBfFc0m+gOpk"
testDhSecret :: C.DhSecretX25519
testDhSecret = "01234567890123456789012345678901"
rcvQueue1 :: RcvQueue
rcvQueue1 =
RcvQueue
{ server = SMPServer "smp.simplex.im" (Just "5223") testKeyHash,
{ server = SMPServer "smp.simplex.im" "5223" testKeyHash,
rcvId = "1234",
connAlias = "conn1",
rcvPrivateKey = C.safePrivateKey (1, 2, 3),
rcvPrivateKey = testPrivateSignKey,
rcvDhSecret = testDhSecret,
e2ePrivKey = testPrivDhKey,
e2eDhSecret = Nothing,
sndId = Just "2345",
sndKey = Nothing,
decryptKey = C.safePrivateKey (1, 2, 3),
verifyKey = Nothing,
status = New
}
sndQueue1 :: SndQueue
sndQueue1 =
SndQueue
{ server = SMPServer "smp.simplex.im" (Just "5223") testKeyHash,
{ server = SMPServer "smp.simplex.im" "5223" testKeyHash,
sndId = "3456",
connAlias = "conn1",
sndPrivateKey = C.safePrivateKey (1, 2, 3),
encryptKey = C.PublicKey $ R.PublicKey 1 2 3,
signKey = C.safePrivateKey (1, 2, 3),
sndPrivateKey = testPrivateSignKey,
e2eDhSecret = testDhSecret,
status = New
}
testCreateRcvConn :: SpecWith SQLiteStore
testCreateRcvConn =
it "should create RcvConnection and add SndQueue" $ \store -> do
createRcvConn store rcvQueue1
`returnsResult` ()
g <- newTVarIO =<< drgNew
createRcvConn store g cData1 rcvQueue1 SCMInvitation
`returnsResult` "conn1"
getConn store "conn1"
`returnsResult` SomeConn SCRcv (RcvConnection "conn1" rcvQueue1)
`returnsResult` SomeConn SCRcv (RcvConnection cData1 rcvQueue1)
upgradeRcvConnToDuplex store "conn1" sndQueue1
`returnsResult` ()
getConn store "conn1"
`returnsResult` SomeConn SCDuplex (DuplexConnection "conn1" rcvQueue1 sndQueue1)
`returnsResult` SomeConn SCDuplex (DuplexConnection cData1 rcvQueue1 sndQueue1)
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 SCMInvitation
getConn store connId
`returnsResult` SomeConn SCRcv (RcvConnection cData1 {connId} rcvQueue1)
upgradeRcvConnToDuplex store connId sndQueue1
`returnsResult` ()
getConn store connId
`returnsResult` SomeConn SCDuplex (DuplexConnection cData1 {connId} rcvQueue1 sndQueue1)
testCreateRcvConnDuplicate :: SpecWith SQLiteStore
testCreateRcvConnDuplicate =
it "should throw error on attempt to create duplicate RcvConnection" $ \store -> do
_ <- runExceptT $ createRcvConn store rcvQueue1
createRcvConn store rcvQueue1
g <- newTVarIO =<< drgNew
_ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 SCMInvitation
createRcvConn store g cData1 rcvQueue1 SCMInvitation
`throwsError` SEConnDuplicate
testCreateSndConn :: SpecWith SQLiteStore
testCreateSndConn =
it "should create SndConnection and add RcvQueue" $ \store -> do
createSndConn store sndQueue1
`returnsResult` ()
g <- newTVarIO =<< drgNew
createSndConn store g cData1 sndQueue1
`returnsResult` "conn1"
getConn store "conn1"
`returnsResult` SomeConn SCSnd (SndConnection "conn1" sndQueue1)
`returnsResult` SomeConn SCSnd (SndConnection cData1 sndQueue1)
upgradeSndConnToDuplex store "conn1" rcvQueue1
`returnsResult` ()
getConn store "conn1"
`returnsResult` SomeConn SCDuplex (DuplexConnection "conn1" rcvQueue1 sndQueue1)
`returnsResult` SomeConn SCDuplex (DuplexConnection cData1 rcvQueue1 sndQueue1)
testCreateSndConnRandomID :: SpecWith SQLiteStore
testCreateSndConnRandomID =
it "should create SndConnection and add RcvQueue with random ID" $ \store -> do
g <- newTVarIO =<< drgNew
Right connId <- runExceptT $ createSndConn store g cData1 {connId = ""} sndQueue1
getConn store connId
`returnsResult` SomeConn SCSnd (SndConnection cData1 {connId} sndQueue1)
upgradeSndConnToDuplex store connId rcvQueue1
`returnsResult` ()
getConn store connId
`returnsResult` SomeConn SCDuplex (DuplexConnection cData1 {connId} rcvQueue1 sndQueue1)
testCreateSndConnDuplicate :: SpecWith SQLiteStore
testCreateSndConnDuplicate =
it "should throw error on attempt to create duplicate SndConnection" $ \store -> do
_ <- runExceptT $ createSndConn store sndQueue1
createSndConn store sndQueue1
g <- newTVarIO =<< drgNew
_ <- runExceptT $ createSndConn store g cData1 sndQueue1
createSndConn store g cData1 sndQueue1
`throwsError` SEConnDuplicate
testGetAllConnAliases :: SpecWith SQLiteStore
testGetAllConnAliases =
it "should get all conn aliases" $ \store -> do
_ <- runExceptT $ createRcvConn store rcvQueue1
_ <- runExceptT $ createSndConn store sndQueue1 {connAlias = "conn2"}
getAllConnAliases store
`returnsResult` ["conn1" :: ConnAlias, "conn2" :: ConnAlias]
testGetRcvConn :: SpecWith SQLiteStore
testGetRcvConn =
it "should get connection using rcv queue id and server" $ \store -> do
let smpServer = SMPServer "smp.simplex.im" (Just "5223") testKeyHash
let smpServer = SMPServer "smp.simplex.im" "5223" testKeyHash
let recipientId = "1234"
_ <- runExceptT $ createRcvConn store rcvQueue1
g <- newTVarIO =<< drgNew
_ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 SCMInvitation
getRcvConn store smpServer recipientId
`returnsResult` SomeConn SCRcv (RcvConnection (connAlias (rcvQueue1 :: RcvQueue)) rcvQueue1)
`returnsResult` SomeConn SCRcv (RcvConnection cData1 rcvQueue1)
testDeleteRcvConn :: SpecWith SQLiteStore
testDeleteRcvConn =
it "should create RcvConnection and delete it" $ \store -> do
_ <- runExceptT $ createRcvConn store rcvQueue1
g <- newTVarIO =<< drgNew
_ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 SCMInvitation
getConn store "conn1"
`returnsResult` SomeConn SCRcv (RcvConnection "conn1" rcvQueue1)
`returnsResult` SomeConn SCRcv (RcvConnection cData1 rcvQueue1)
deleteConn store "conn1"
`returnsResult` ()
-- TODO check queues are deleted as well
@@ -228,9 +270,10 @@ testDeleteRcvConn =
testDeleteSndConn :: SpecWith SQLiteStore
testDeleteSndConn =
it "should create SndConnection and delete it" $ \store -> do
_ <- runExceptT $ createSndConn store sndQueue1
g <- newTVarIO =<< drgNew
_ <- runExceptT $ createSndConn store g cData1 sndQueue1
getConn store "conn1"
`returnsResult` SomeConn SCSnd (SndConnection "conn1" sndQueue1)
`returnsResult` SomeConn SCSnd (SndConnection cData1 sndQueue1)
deleteConn store "conn1"
`returnsResult` ()
-- TODO check queues are deleted as well
@@ -240,10 +283,11 @@ testDeleteSndConn =
testDeleteDuplexConn :: SpecWith SQLiteStore
testDeleteDuplexConn =
it "should create DuplexConnection and delete it" $ \store -> do
_ <- runExceptT $ createRcvConn store rcvQueue1
g <- newTVarIO =<< drgNew
_ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 SCMInvitation
_ <- runExceptT $ upgradeRcvConnToDuplex store "conn1" sndQueue1
getConn store "conn1"
`returnsResult` SomeConn SCDuplex (DuplexConnection "conn1" rcvQueue1 sndQueue1)
`returnsResult` SomeConn SCDuplex (DuplexConnection cData1 rcvQueue1 sndQueue1)
deleteConn store "conn1"
`returnsResult` ()
-- TODO check queues are deleted as well
@@ -253,15 +297,14 @@ testDeleteDuplexConn =
testUpgradeRcvConnToDuplex :: SpecWith SQLiteStore
testUpgradeRcvConnToDuplex =
it "should throw error on attempt to add SndQueue to SndConnection or DuplexConnection" $ \store -> do
_ <- runExceptT $ createSndConn store sndQueue1
g <- newTVarIO =<< drgNew
_ <- runExceptT $ createSndConn store g cData1 sndQueue1
let anotherSndQueue =
SndQueue
{ server = SMPServer "smp.simplex.im" (Just "5223") testKeyHash,
{ server = SMPServer "smp.simplex.im" "5223" testKeyHash,
sndId = "2345",
connAlias = "conn1",
sndPrivateKey = C.safePrivateKey (1, 2, 3),
encryptKey = C.PublicKey $ R.PublicKey 1 2 3,
signKey = C.safePrivateKey (1, 2, 3),
sndPrivateKey = testPrivateSignKey,
e2eDhSecret = testDhSecret,
status = New
}
upgradeRcvConnToDuplex store "conn1" anotherSndQueue
@@ -273,17 +316,17 @@ testUpgradeRcvConnToDuplex =
testUpgradeSndConnToDuplex :: SpecWith SQLiteStore
testUpgradeSndConnToDuplex =
it "should throw error on attempt to add RcvQueue to RcvConnection or DuplexConnection" $ \store -> do
_ <- runExceptT $ createRcvConn store rcvQueue1
g <- newTVarIO =<< drgNew
_ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 SCMInvitation
let anotherRcvQueue =
RcvQueue
{ server = SMPServer "smp.simplex.im" (Just "5223") testKeyHash,
{ server = SMPServer "smp.simplex.im" "5223" testKeyHash,
rcvId = "3456",
connAlias = "conn1",
rcvPrivateKey = C.safePrivateKey (1, 2, 3),
rcvPrivateKey = testPrivateSignKey,
rcvDhSecret = testDhSecret,
e2ePrivKey = testPrivDhKey,
e2eDhSecret = Nothing,
sndId = Just "4567",
sndKey = Nothing,
decryptKey = C.safePrivateKey (1, 2, 3),
verifyKey = Nothing,
status = New
}
upgradeSndConnToDuplex store "conn1" anotherRcvQueue
@@ -295,52 +338,43 @@ testUpgradeSndConnToDuplex =
testSetRcvQueueStatus :: SpecWith SQLiteStore
testSetRcvQueueStatus =
it "should update status of RcvQueue" $ \store -> do
_ <- runExceptT $ createRcvConn store rcvQueue1
g <- newTVarIO =<< drgNew
_ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 SCMInvitation
getConn store "conn1"
`returnsResult` SomeConn SCRcv (RcvConnection "conn1" rcvQueue1)
`returnsResult` SomeConn SCRcv (RcvConnection cData1 rcvQueue1)
setRcvQueueStatus store rcvQueue1 Confirmed
`returnsResult` ()
getConn store "conn1"
`returnsResult` SomeConn SCRcv (RcvConnection "conn1" rcvQueue1 {status = Confirmed})
`returnsResult` SomeConn SCRcv (RcvConnection cData1 rcvQueue1 {status = Confirmed})
testSetSndQueueStatus :: SpecWith SQLiteStore
testSetSndQueueStatus =
it "should update status of SndQueue" $ \store -> do
_ <- runExceptT $ createSndConn store sndQueue1
g <- newTVarIO =<< drgNew
_ <- runExceptT $ createSndConn store g cData1 sndQueue1
getConn store "conn1"
`returnsResult` SomeConn SCSnd (SndConnection "conn1" sndQueue1)
`returnsResult` SomeConn SCSnd (SndConnection cData1 sndQueue1)
setSndQueueStatus store sndQueue1 Confirmed
`returnsResult` ()
getConn store "conn1"
`returnsResult` SomeConn SCSnd (SndConnection "conn1" sndQueue1 {status = Confirmed})
`returnsResult` SomeConn SCSnd (SndConnection cData1 sndQueue1 {status = Confirmed})
testSetQueueStatusDuplex :: SpecWith SQLiteStore
testSetQueueStatusDuplex =
it "should update statuses of RcvQueue and SndQueue in DuplexConnection" $ \store -> do
_ <- runExceptT $ createRcvConn store rcvQueue1
g <- newTVarIO =<< drgNew
_ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 SCMInvitation
_ <- runExceptT $ upgradeRcvConnToDuplex store "conn1" sndQueue1
getConn store "conn1"
`returnsResult` SomeConn SCDuplex (DuplexConnection "conn1" rcvQueue1 sndQueue1)
`returnsResult` SomeConn SCDuplex (DuplexConnection cData1 rcvQueue1 sndQueue1)
setRcvQueueStatus store rcvQueue1 Secured
`returnsResult` ()
getConn store "conn1"
`returnsResult` SomeConn SCDuplex (DuplexConnection "conn1" rcvQueue1 {status = Secured} sndQueue1)
`returnsResult` SomeConn SCDuplex (DuplexConnection cData1 rcvQueue1 {status = Secured} sndQueue1)
setSndQueueStatus store sndQueue1 Confirmed
`returnsResult` ()
getConn store "conn1"
`returnsResult` SomeConn SCDuplex (DuplexConnection "conn1" rcvQueue1 {status = Secured} sndQueue1 {status = Confirmed})
testSetRcvQueueStatusNoQueue :: SpecWith SQLiteStore
testSetRcvQueueStatusNoQueue =
xit "should throw error on attempt to update status of non-existent RcvQueue" $ \store -> do
setRcvQueueStatus store rcvQueue1 Confirmed
`throwsError` SEConnNotFound
testSetSndQueueStatusNoQueue :: SpecWith SQLiteStore
testSetSndQueueStatusNoQueue =
xit "should throw error on attempt to update status of non-existent SndQueue" $ \store -> do
setSndQueueStatus store sndQueue1 Confirmed
`throwsError` SEConnNotFound
`returnsResult` SomeConn SCDuplex (DuplexConnection cData1 rcvQueue1 {status = Secured} sndQueue1 {status = Confirmed})
hw :: ByteString
hw = encodeUtf8 "Hello world!"
@@ -351,31 +385,36 @@ ts = UTCTime (fromGregorian 2021 02 24) (secondsToDiffTime 0)
mkRcvMsgData :: InternalId -> InternalRcvId -> ExternalSndId -> BrokerId -> MsgHash -> RcvMsgData
mkRcvMsgData internalId internalRcvId externalSndId brokerId internalHash =
RcvMsgData
{ internalId,
internalRcvId,
internalTs = ts,
senderMeta = (externalSndId, ts),
brokerMeta = (brokerId, ts),
{ internalRcvId,
msgMeta =
MsgMeta
{ integrity = MsgOk,
recipient = (unId internalId, ts),
sndMsgId = externalSndId,
broker = (brokerId, ts)
},
msgType = A_MSG_,
msgBody = hw,
internalHash,
externalPrevSndHash = "hash_from_sender",
msgIntegrity = MsgOk
externalPrevSndHash = "hash_from_sender"
}
testCreateRcvMsg' :: SQLiteStore -> PrevExternalSndId -> PrevRcvMsgHash -> RcvQueue -> RcvMsgData -> Expectation
testCreateRcvMsg' store expectedPrevSndId expectedPrevHash rcvQueue rcvMsgData@RcvMsgData {..} = do
updateRcvIds store rcvQueue
`returnsResult` (internalId, internalRcvId, expectedPrevSndId, expectedPrevHash)
createRcvMsg store rcvQueue rcvMsgData
testCreateRcvMsg_ :: SQLiteStore -> PrevExternalSndId -> PrevRcvMsgHash -> ConnId -> RcvMsgData -> Expectation
testCreateRcvMsg_ st expectedPrevSndId expectedPrevHash connId rcvMsgData@RcvMsgData {..} = do
let MsgMeta {recipient = (internalId, _)} = msgMeta
updateRcvIds st connId
`returnsResult` (InternalId internalId, internalRcvId, expectedPrevSndId, expectedPrevHash)
createRcvMsg st connId rcvMsgData
`returnsResult` ()
testCreateRcvMsg :: SpecWith SQLiteStore
testCreateRcvMsg =
it "should reserve internal ids and create a RcvMsg" $ \store -> do
_ <- runExceptT $ createRcvConn store rcvQueue1
-- TODO getMsg to check message
testCreateRcvMsg' store 0 "" rcvQueue1 $ mkRcvMsgData (InternalId 1) (InternalRcvId 1) 1 "1" "hash_dummy"
testCreateRcvMsg' store 1 "hash_dummy" rcvQueue1 $ mkRcvMsgData (InternalId 2) (InternalRcvId 2) 2 "2" "new_hash_dummy"
it "should reserve internal ids and create a RcvMsg" $ \st -> do
g <- newTVarIO =<< drgNew
let ConnData {connId} = cData1
_ <- runExceptT $ createRcvConn st g cData1 rcvQueue1 SCMInvitation
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"
mkSndMsgData :: InternalId -> InternalSndId -> MsgHash -> SndMsgData
mkSndMsgData internalId internalSndId internalHash =
@@ -383,33 +422,38 @@ mkSndMsgData internalId internalSndId internalHash =
{ internalId,
internalSndId,
internalTs = ts,
msgType = A_MSG_,
msgBody = hw,
internalHash
internalHash,
prevMsgHash = internalHash
}
testCreateSndMsg' :: SQLiteStore -> PrevSndMsgHash -> SndQueue -> SndMsgData -> Expectation
testCreateSndMsg' store expectedPrevHash sndQueue sndMsgData@SndMsgData {..} = do
updateSndIds store sndQueue
testCreateSndMsg_ :: SQLiteStore -> PrevSndMsgHash -> ConnId -> SndMsgData -> Expectation
testCreateSndMsg_ store expectedPrevHash connId sndMsgData@SndMsgData {..} = do
updateSndIds store connId
`returnsResult` (internalId, internalSndId, expectedPrevHash)
createSndMsg store sndQueue sndMsgData
createSndMsg store connId sndMsgData
`returnsResult` ()
testCreateSndMsg :: SpecWith SQLiteStore
testCreateSndMsg =
it "should create a SndMsg and return InternalId and PrevSndMsgHash" $ \store -> do
_ <- runExceptT $ createSndConn store sndQueue1
-- TODO getMsg to check message
testCreateSndMsg' store "" sndQueue1 $ mkSndMsgData (InternalId 1) (InternalSndId 1) "hash_dummy"
testCreateSndMsg' store "hash_dummy" sndQueue1 $ mkSndMsgData (InternalId 2) (InternalSndId 2) "new_hash_dummy"
g <- newTVarIO =<< drgNew
let ConnData {connId} = cData1
_ <- runExceptT $ createSndConn store g cData1 sndQueue1
testCreateSndMsg_ store "" connId $ mkSndMsgData (InternalId 1) (InternalSndId 1) "hash_dummy"
testCreateSndMsg_ store "hash_dummy" connId $ mkSndMsgData (InternalId 2) (InternalSndId 2) "new_hash_dummy"
testCreateRcvAndSndMsgs :: SpecWith SQLiteStore
testCreateRcvAndSndMsgs =
it "should create multiple RcvMsg and SndMsg, correctly ordering internal Ids and returning previous state" $ \store -> do
_ <- runExceptT $ createRcvConn store rcvQueue1
g <- newTVarIO =<< drgNew
let ConnData {connId} = cData1
_ <- runExceptT $ createRcvConn store g cData1 rcvQueue1 SCMInvitation
_ <- runExceptT $ upgradeRcvConnToDuplex store "conn1" sndQueue1
testCreateRcvMsg' store 0 "" rcvQueue1 $ mkRcvMsgData (InternalId 1) (InternalRcvId 1) 1 "1" "rcv_hash_1"
testCreateRcvMsg' store 1 "rcv_hash_1" rcvQueue1 $ mkRcvMsgData (InternalId 2) (InternalRcvId 2) 2 "2" "rcv_hash_2"
testCreateSndMsg' store "" sndQueue1 $ mkSndMsgData (InternalId 3) (InternalSndId 1) "snd_hash_1"
testCreateRcvMsg' store 2 "rcv_hash_2" rcvQueue1 $ mkRcvMsgData (InternalId 4) (InternalRcvId 3) 3 "3" "rcv_hash_3"
testCreateSndMsg' store "snd_hash_1" sndQueue1 $ mkSndMsgData (InternalId 5) (InternalSndId 2) "snd_hash_2"
testCreateSndMsg' store "snd_hash_2" sndQueue1 $ mkSndMsgData (InternalId 6) (InternalSndId 3) "snd_hash_3"
testCreateRcvMsg_ store 0 "" connId $ mkRcvMsgData (InternalId 1) (InternalRcvId 1) 1 "1" "rcv_hash_1"
testCreateRcvMsg_ store 1 "rcv_hash_1" connId $ mkRcvMsgData (InternalId 2) (InternalRcvId 2) 2 "2" "rcv_hash_2"
testCreateSndMsg_ store "" connId $ mkSndMsgData (InternalId 3) (InternalSndId 1) "snd_hash_1"
testCreateRcvMsg_ store 2 "rcv_hash_2" connId $ mkRcvMsgData (InternalId 4) (InternalRcvId 3) 3 "3" "rcv_hash_3"
testCreateSndMsg_ store "snd_hash_1" connId $ mkSndMsgData (InternalId 5) (InternalSndId 2) "snd_hash_2"
testCreateSndMsg_ store "snd_hash_2" connId $ mkSndMsgData (InternalId 6) (InternalSndId 3) "snd_hash_3"
+46
View File
@@ -0,0 +1,46 @@
{-# LANGUAGE OverloadedStrings #-}
module CoreTests.EncodingTests where
import Data.Bits (shiftR)
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.ByteString.Internal (w2c)
import Data.Int (Int64)
import Data.Time.Clock.System (SystemTime (..), getSystemTime, utcToSystemTime)
import Data.Time.ISO8601 (parseISO8601)
import Simplex.Messaging.Encoding
import Simplex.Messaging.Parsers (parseAll)
import Test.Hspec
import Test.Hspec.QuickCheck (modifyMaxSuccess)
import Test.QuickCheck
int64 :: Int64
int64 = 1234567890123456789
s64 :: ByteString
s64 = B.pack $ map (w2c . fromIntegral . (int64 `shiftR`)) [56, 48, 40, 32, 24, 16, 8, 0]
encodingTests :: Spec
encodingTests = modifyMaxSuccess (const 1000) $ do
describe "Encoding Int64" $ do
it "should encode and decode Int64 example" $ do
s64 `shouldBe` "\17\34\16\244\125\233\129\21"
smpEncode int64 `shouldBe` s64
parseAll smpP s64 `shouldBe` Right int64
it "parse(encode(Int64) should equal the same Int64" . property $
\i -> parseAll smpP (smpEncode i) == Right (i :: Int64)
describe "Encoding SystemTime" $ do
it "should encode and decode SystemTime" $ do
t <- getSystemTime
testSystemTime t
Just t' <- pure $ utcToSystemTime <$> parseISO8601 "2022-01-01T10:24:05.000Z"
systemSeconds t' `shouldBe` 1641032645
testSystemTime t'
it "parse(encode(SystemTime) should equal the same Int64" . property $
\i -> parseAll smpP (smpEncode i) == Right (i :: Int64)
where
testSystemTime :: SystemTime -> Expectation
testSystemTime t = do
smpEncode t `shouldBe` smpEncode (systemSeconds t)
parseAll smpP (smpEncode t) `shouldBe` Right t {systemNanoseconds = 0}
@@ -1,8 +1,9 @@
module ProtocolErrorTests where
module CoreTests.ProtocolErrorTests where
import Simplex.Messaging.Agent.Protocol (AgentErrorType, agentErrorTypeP, serializeAgentError)
import Simplex.Messaging.Agent.Protocol (AgentErrorType)
import Simplex.Messaging.Parsers (parseAll)
import Simplex.Messaging.Protocol (ErrorType, errorTypeP, serializeErrorType)
import Simplex.Messaging.Protocol (ErrorType)
import Simplex.Messaging.Encoding.String
import Test.Hspec
import Test.Hspec.QuickCheck (modifyMaxSuccess)
import Test.QuickCheck
@@ -11,8 +12,8 @@ protocolErrorTests :: Spec
protocolErrorTests = modifyMaxSuccess (const 1000) $ do
describe "errors parsing / serializing" $ do
it "should parse SMP protocol errors" . property $ \err ->
parseAll errorTypeP (serializeErrorType err)
parseAll strP (strEncode err)
== Right (err :: ErrorType)
it "should parse SMP agent errors" . property $ \err ->
parseAll agentErrorTypeP (serializeAgentError err)
parseAll strP (strEncode err)
== Right (err :: AgentErrorType)
+58
View File
@@ -0,0 +1,58 @@
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
module CoreTests.VersionRangeTests where
import GHC.Generics (Generic)
import Generic.Random (genericArbitraryU)
import Simplex.Messaging.Version
import Test.Hspec
import Test.Hspec.QuickCheck (modifyMaxSuccess)
import Test.QuickCheck
data V = V1 | V2 | V3 | V4 | V5 deriving (Eq, Enum, Ord, Generic, Show)
instance Arbitrary V where arbitrary = genericArbitraryU
versionRangeTests :: Spec
versionRangeTests = modifyMaxSuccess (const 1000) $ do
describe "VersionRange construction" $ do
it "should fail on invalid range" $ do
vr 1 1 `shouldBe` vr 1 1
vr 1 2 `shouldBe` vr 1 2
(pure $! vr 2 1) `shouldThrow` anyErrorCall
describe "compatible version" $ do
it "should choose mutually compatible max version" $ do
(vr 1 1, vr 1 1) `compatible` Just 1
(vr 1 1, vr 1 2) `compatible` Just 1
(vr 1 2, vr 1 2) `compatible` Just 2
(vr 1 2, vr 2 3) `compatible` Just 2
(vr 1 3, vr 2 3) `compatible` Just 3
(vr 1 3, vr 2 4) `compatible` Just 3
(vr 1 2, vr 3 4) `compatible` Nothing
it "should check if version is compatible" $ do
isCompatible (1 :: Version) (vr 1 2) `shouldBe` True
isCompatible (2 :: Version) (vr 1 2) `shouldBe` True
isCompatible (2 :: Version) (vr 1 1) `shouldBe` False
isCompatible (1 :: Version) (vr 2 2) `shouldBe` False
it "compatibleVersion should pass isCompatible check" . property $
\((min1, max1) :: (V, V)) ((min2, max2) :: (V, V)) ->
min1 > max1 || min2 > max2 -- one of ranges is invalid, skip testing it
|| let w = fromIntegral . fromEnum
vr1 = mkVersionRange (w min1) (w max1) :: VersionRange
vr2 = mkVersionRange (w min2) (w max2) :: VersionRange
in case compatibleVersion vr1 vr2 of
Just (Compatible v) -> v `isCompatible` vr1 && v `isCompatible` vr2
_ -> True
where
vr = mkVersionRange
compatible :: (VersionRange, VersionRange) -> Maybe Version -> Expectation
(vr1, vr2) `compatible` v = do
(vr1, vr2) `checkCompatible` v
(vr2, vr1) `checkCompatible` v
(vr1, vr2) `checkCompatible` v =
case compatibleVersion vr1 vr2 of
Just (Compatible v') -> Just v' `shouldBe` v
Nothing -> Nothing `shouldBe` v
+40 -20
View File
@@ -1,4 +1,3 @@
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE OverloadedStrings #-}
@@ -21,11 +20,13 @@ import SMPClient
withSmpServerOn,
withSmpServerThreadOn,
)
import Simplex.Messaging.Agent (runSMPAgentBlocking)
import Simplex.Messaging.Agent.Env.SQLite
import Simplex.Messaging.Agent.Protocol
import Simplex.Messaging.Agent.RetryInterval
import Simplex.Messaging.Agent.Server (runSMPAgentBlocking)
import Simplex.Messaging.Client (SMPClientConfig (..), smpDefaultConfig)
import Simplex.Messaging.Transport
import Simplex.Messaging.Transport.Client
import Test.Hspec
import UnliftIO.Concurrent
import UnliftIO.Directory
@@ -95,11 +96,15 @@ smpAgentTestN_1 n test' = runSmpAgentTestN_1 n test' `shouldReturn` ()
smpAgentTest2_2_2 :: forall c. Transport c => (c -> c -> IO ()) -> Expectation
smpAgentTest2_2_2 test' =
withSmpServerOn (transport @c) testPort2 $
smpAgentTestN
[ (agentTestPort, testPort, testDB),
(agentTestPort2, testPort2, testDB2)
]
_test
smpAgentTest2_2_2_needs_server test'
smpAgentTest2_2_2_needs_server :: forall c. Transport c => (c -> c -> IO ()) -> Expectation
smpAgentTest2_2_2_needs_server test' =
smpAgentTestN
[ (agentTestPort, testPort, testDB),
(agentTestPort2, testPort2, testDB2)
]
_test
where
_test [h1, h2] = test' h1 h2
_test _ = error "expected 2 handles"
@@ -139,29 +144,43 @@ smpAgentTest3_1_1 test' = smpAgentTestN_1 3 _test
_test [h1, h2, h3] = test' h1 h2 h3
_test _ = error "expected 3 handles"
smpAgentTest1_1_1 :: forall c. Transport c => (c -> IO ()) -> Expectation
smpAgentTest1_1_1 test' =
smpAgentTestN
[(agentTestPort2, testPort2, testDB2)]
_test
where
_test [h] = test' h
_test _ = error "expected 1 handle"
cfg :: AgentConfig
cfg =
AgentConfig
defaultAgentConfig
{ tcpPort = agentTestPort,
smpServers = L.fromList ["localhost:5000#KXNE1m2E1m0lm92WGKet9CL6+lO742Vy5G6nsrkvgs8="],
rsaKeySize = 2048 `div` 8,
connIdBytes = 12,
smpServers = L.fromList ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:5001"],
tbqSize = 1,
dbFile = testDB,
smpCfg =
smpDefaultConfig
{ qSize = 1,
defaultTransport = (testPort, transport @TCP),
defaultTransport = (testPort, transport @TLS),
tcpTimeout = 500_000
}
},
reconnectInterval = (reconnectInterval defaultAgentConfig) {initialInterval = 50_000},
caCertificateFile = "tests/fixtures/ca.crt",
privateKeyFile = "tests/fixtures/server.key",
certificateFile = "tests/fixtures/server.crt"
}
withSmpAgentThreadOn :: (MonadUnliftIO m, MonadRandom m) => ATransport -> (ServiceName, ServiceName, String) -> (ThreadId -> m a) -> m a
withSmpAgentThreadOn t (port', smpPort', db') =
let cfg' = cfg {tcpPort = port', dbFile = db', smpServers = L.fromList [SMPServer "localhost" (Just smpPort') testKeyHash]}
withSmpAgentThreadOn_ :: (MonadUnliftIO m, MonadRandom m) => ATransport -> (ServiceName, ServiceName, String) -> m () -> (ThreadId -> m a) -> m a
withSmpAgentThreadOn_ t (port', smpPort', db') afterProcess =
let cfg' = cfg {tcpPort = port', dbFile = db', smpServers = L.fromList [SMPServer "localhost" smpPort' testKeyHash]}
in serverBracket
(\started -> runSMPAgentBlocking t started cfg')
(removeFile db')
afterProcess
withSmpAgentThreadOn :: (MonadUnliftIO m, MonadRandom m) => ATransport -> (ServiceName, ServiceName, String) -> (ThreadId -> m a) -> m a
withSmpAgentThreadOn t a@(_, _, db') = withSmpAgentThreadOn_ t a $ removeFile db'
withSmpAgentOn :: (MonadUnliftIO m, MonadRandom m) => ATransport -> (ServiceName, ServiceName, String) -> m a -> m a
withSmpAgentOn t (port', smpPort', db') = withSmpAgentThreadOn t (port', smpPort', db') . const
@@ -171,11 +190,12 @@ withSmpAgent t = withSmpAgentOn t (agentTestPort, testPort, testDB)
testSMPAgentClientOn :: (Transport c, MonadUnliftIO m) => ServiceName -> (c -> m a) -> m a
testSMPAgentClientOn port' client = do
runTransportClient agentTestHost port' $ \h -> do
runTransportClient agentTestHost port' testKeyHash $ \h -> do
line <- liftIO $ getLn h
if line == "Welcome to SMP v0.3.2 agent"
if line == "Welcome to SMP agent v" <> B.pack simplexMQVersion
then client h
else error $ "wrong welcome message: " <> B.unpack line
else do
error $ "wrong welcome message: " <> B.unpack line
testSMPAgentClient :: (Transport c, MonadUnliftIO m) => (c -> m a) -> m a
testSMPAgentClient = testSMPAgentClientOn agentTestPort
+42 -60
View File
@@ -1,5 +1,6 @@
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE OverloadedStrings #-}
@@ -8,19 +9,19 @@
module SMPClient where
import Control.Monad (void)
import Control.Monad.Except (runExceptT)
import Control.Monad.IO.Unlift
import Crypto.Random
import Data.ByteString.Base64 (encode)
import qualified Data.ByteString.Char8 as B
import Data.ByteString.Char8 (ByteString)
import Network.Socket
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding
import Simplex.Messaging.Protocol
import Simplex.Messaging.Server (runSMPServerBlocking)
import Simplex.Messaging.Server.Env.STM
import Simplex.Messaging.Server.StoreLog (openReadStoreLog)
import Simplex.Messaging.Transport
import Simplex.Messaging.Transport.Client
import Test.Hspec
import UnliftIO.Concurrent
import qualified UnliftIO.Exception as E
@@ -31,23 +32,20 @@ testHost :: HostName
testHost = "localhost"
testPort :: ServiceName
testPort = "5000"
testPort = "5001"
testPort2 :: ServiceName
testPort2 = "5001"
testPort2 = "5002"
testKeyHashStr :: B.ByteString
testKeyHashStr = "KXNE1m2E1m0lm92WGKet9CL6+lO742Vy5G6nsrkvgs8="
testKeyHash :: Maybe C.KeyHash
testKeyHash = Just "KXNE1m2E1m0lm92WGKet9CL6+lO742Vy5G6nsrkvgs8="
testKeyHash :: C.KeyHash
testKeyHash = "LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI="
testStoreLogFile :: FilePath
testStoreLogFile = "tests/tmp/smp-server-store.log"
testSMPClient :: (Transport c, MonadUnliftIO m) => (THandle c -> m a) -> m a
testSMPClient client =
runTransportClient testHost testPort $ \h ->
runTransportClient testHost testPort testKeyHash $ \h ->
liftIO (runExceptT $ clientHandshake h testKeyHash) >>= \case
Right th -> client th
Left e -> error $ show e
@@ -57,53 +55,28 @@ cfg =
ServerConfig
{ transports = undefined,
tbqSize = 1,
queueIdBytes = 12,
msgIdBytes = 6,
serverTbqSize = 1,
msgQueueQuota = 4,
queueIdBytes = 24,
msgIdBytes = 24,
storeLog = Nothing,
serverPrivateKey =
-- full RSA private key (only for tests)
"MIIFIwIBAAKCAQEArZyrri/NAwt5buvYjwu+B/MQeJUszDBpRgVqNddlI9kNwDXu\
\kaJ8chEhrtaUgXeSWGooWwqjXEUQE6RVbCC6QVo9VEBSP4xFwVVd9Fj7OsgfcXXh\
\AqWxfctDcBZQ5jTUiJpdBc+Vz2ZkumVNl0W+j9kWm9nfkMLQj8c0cVSDxz4OKpZb\
\qFuj0uzHkis7e7wsrKSKWLPg3M5ZXPZM1m9qn7SfJzDRDfJifamxWI7uz9XK2+Dp\
\NkUQlGQgFJEv1cKN88JAwIqZ1s+TAQMQiB+4QZ2aNfSqGEzRJN7FMCKRK7pM0A9A\
\PCnijyuImvKFxTdk8Bx1q+XNJzsY6fBrLWJZ+QKBgQCySG4tzlcEm+tOVWRcwrWh\
\6zsczGZp9mbf9c8itRx6dlldSYuDG1qnddL70wuAZF2AgS1JZgvcRZECoZRoWP5q\
\Kq2wvpTIYjFPpC39lxgUoA/DXKVKZZdan+gwaVPAPT54my1CS32VrOiAY4gVJ3LJ\
\Mn1/FqZXUFQA326pau3loQKCAQEAoljmJMp88EZoy3HlHUbOjl5UEhzzVsU1TnQi\
\QmPm+aWRe2qelhjW4aTvSVE5mAUJsN6UWTeMf4uvM69Z9I5pfw2pEm8x4+GxRibY\
\iiwF2QNaLxxmzEHm1zQQPTgb39o8mgklhzFPill0JsnL3f6IkVwjFJofWSmpqEGs\
\dFSMRSXUTVXh1p/o7QZrhpwO/475iWKVS7o48N/0Xp513re3aXw+DRNuVnFEaBIe\
\TLvWM9Czn16ndAu1HYiTBuMvtRbAWnGZxU8ewzF4wlWK5tdIL5PTJDd1VhZJAKtB\
\npDvJpwxzKmjAhcTmjx0ckMIWtdVaOVm/2gWCXDty2FEdg7koQKBgQDOUUguJ/i7\
\q0jldWYRnVkotKnpInPdcEaodrehfOqYEHnvro9xlS6OeAS4Vz5AdH45zQ/4J3bV\
\2cH66tNr18ebM9nL//t5G69i89R9W7szyUxCI3LmAIdi3oSEbmz5GQBaw4l6h9Wi\
\n4FmFQaAXZrjQfO2qJcAHvWRsMp2pmqAGwKBgQDXaza0DRsKWywWznsHcmHa0cx8\
\I4jxqGaQmLO7wBJRP1NSFrywy1QfYrVX9CTLBK4V3F0PCgZ01Qv94751CzN43TgF\
\ebd/O9r5NjNTnOXzdWqETbCffLGd6kLgCMwPQWpM9ySVjXHWCGZsRAnF2F6M1O32\
\43StIifvwJQFqSM3ewKBgCaW6y7sRY90Ua7283RErezd9EyT22BWlDlACrPu3FNC\
\LtBf1j43uxBWBQrMLsHe2GtTV0xt9m0MfwZsm2gSsXcm4Xi4DJgfN+Z7rIlyy9UY\
\PCDSdZiU1qSr+NrffDrXlfiAM1cUmCdUX7eKjp/ltkUHNaOGfSn5Pdr3MkAiD/Hf\
\AoGBAKIdKCuOwuYlwjS9J+IRGuSSM4o+OxQdwGmcJDTCpyWb5dEk68e7xKIna3zf\
\jc+H+QdMXv1nkRK9bZgYheXczsXaNZUSTwpxaEldzVD3hNvsXSgJRy9fqHwA4PBq\
\vqiBHoO3RNbqg+2rmTMfDuXreME3S955ZiPZm4Z+T8Hj52mPAoGAQm5QH/gLFtY5\
\+znqU/0G8V6BKISCQMxbbmTQVcTgGySrP2gVd+e4MWvUttaZykhWqs8rpr7mgpIY\
\hul7Swx0SHFN3WpXu8uj+B6MLpRcCbDHO65qU4kQLs+IaXXsuuTjMvJ5LwjkZVrQ\
\TmKzSAw7iVWwEUZR/PeiEKazqrpp9VU="
caCertificateFile = "tests/fixtures/ca.crt",
privateKeyFile = "tests/fixtures/server.key",
certificateFile = "tests/fixtures/server.crt"
}
withSmpServerStoreLogOn :: (MonadUnliftIO m, MonadRandom m) => ATransport -> ServiceName -> (ThreadId -> m a) -> m a
withSmpServerStoreLogOn t port client = do
withSmpServerStoreLogOn t port' client = do
s <- liftIO $ openReadStoreLog testStoreLogFile
serverBracket
(\started -> runSMPServerBlocking started cfg {transports = [(port, t)], storeLog = Just s})
(\started -> runSMPServerBlocking started cfg {transports = [(port', t)], storeLog = Just s})
(pure ())
client
withSmpServerThreadOn :: (MonadUnliftIO m, MonadRandom m) => ATransport -> ServiceName -> (ThreadId -> m a) -> m a
withSmpServerThreadOn t port =
withSmpServerThreadOn t port' =
serverBracket
(\started -> runSMPServerBlocking started cfg {transports = [(port, t)]})
(\started -> runSMPServerBlocking started cfg {transports = [(port', t)]})
(pure ())
serverBracket :: MonadUnliftIO m => (TMVar Bool -> m ()) -> m () -> (ThreadId -> m a) -> m a
@@ -120,7 +93,7 @@ serverBracket process afterProcess f = do
_ -> pure ()
withSmpServerOn :: (MonadUnliftIO m, MonadRandom m) => ATransport -> ServiceName -> m a -> m a
withSmpServerOn t port = withSmpServerThreadOn t port . const
withSmpServerOn t port' = withSmpServerThreadOn t port' . const
withSmpServer :: (MonadUnliftIO m, MonadRandom m) => ATransport -> m a -> m a
withSmpServer t = withSmpServerOn t testPort
@@ -135,8 +108,21 @@ runSmpTestN nClients test = withSmpServer (transport @c) $ run nClients []
run 0 hs = test hs
run n hs = testSMPClient $ \h -> run (n - 1) (h : hs)
smpServerTest :: forall c. Transport c => TProxy c -> RawTransmission -> IO RawTransmission
smpServerTest _ cmd = runSmpTest $ \(h :: THandle c) -> tPutRaw h cmd >> tGetRaw h
smpServerTest ::
forall c smp.
(Transport c, Encoding smp) =>
TProxy c ->
(Maybe C.ASignature, ByteString, ByteString, smp) ->
IO (Maybe C.ASignature, ByteString, ByteString, BrokerMsg)
smpServerTest _ t = runSmpTest $ \h -> tPut' h t >> tGet' h
where
tPut' h (sig, corrId, queueId, smp) = do
let t' = smpEncode (sessionId (h :: THandle c), corrId, queueId, smp)
Right () <- tPut h (sig, t')
pure ()
tGet' h = do
(Nothing, _, (CorrId corrId, qId, Right cmd)) <- tGet h
pure (Nothing, corrId, qId, cmd)
smpTest :: Transport c => TProxy c -> (THandle c -> IO ()) -> Expectation
smpTest _ test' = runSmpTest test' `shouldReturn` ()
@@ -156,12 +142,8 @@ smpTest3 _ test' = smpTestN 3 _test
_test [h1, h2, h3] = test' h1 h2 h3
_test _ = error "expected 3 handles"
tPutRaw :: Transport c => THandle c -> RawTransmission -> IO ()
tPutRaw h (sig, corrId, queueId, command) = do
let t = B.intercalate " " [corrId, queueId, command]
void $ tPut h (C.Signature sig, t)
tGetRaw :: Transport c => THandle c -> IO RawTransmission
tGetRaw h = do
("", (CorrId corrId, qId, Right cmd)) <- tGet fromServer h
pure ("", corrId, encode qId, serializeCommand cmd)
smpTest4 :: Transport c => TProxy c -> (THandle c -> THandle c -> THandle c -> THandle c -> IO ()) -> Expectation
smpTest4 _ test' = smpTestN 4 _test
where
_test [h1, h2, h3, h4] = test' h1 h2 h3 h4
_test _ = error "expected 4 handles"
+305 -204
View File
@@ -1,22 +1,25 @@
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
module ServerTests where
import Control.Concurrent (ThreadId, killThread)
import Control.Concurrent.STM
import Control.Exception (SomeException, try)
import Control.Monad.Except (forM_, runExceptT)
import Control.Monad.Except (forM, forM_, runExceptT)
import Data.ByteString.Base64
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import SMPClient
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol
import Simplex.Messaging.Transport
import System.Directory (removeFile)
@@ -25,36 +28,38 @@ import System.Timeout
import Test.HUnit
import Test.Hspec
rsaKeySize :: Int
rsaKeySize = 2048 `div` 8
serverTests :: ATransport -> Spec
serverTests t = do
describe "SMP syntax" $ syntaxTests t
describe "SMP queues" do
describe "SMP queues" $ do
describe "NEW and KEY commands, SEND messages" $ testCreateSecure t
describe "NEW, OFF and DEL commands, SEND messages" $ testCreateDelete t
describe "SMP messages" do
describe "Stress test" $ stressTest t
describe "SMP messages" $ do
describe "duplex communication over 2 SMP connections" $ testDuplex t
describe "switch subscription to another SMP queue" $ testSwitchSub t
describe "switch subscription to another TCP connection" $ testSwitchSub t
describe "Store log" $ testWithStoreLog t
describe "Timing of AUTH error" $ testTiming t
describe "Message notifications" $ testMessageNotifications t
pattern Resp :: CorrId -> QueueId -> Command 'Broker -> SignedTransmissionOrError
pattern Resp corrId queueId command <- ("", (corrId, queueId, Right (Cmd SBroker command)))
pattern Resp :: CorrId -> QueueId -> BrokerMsg -> SignedTransmission BrokerMsg
pattern Resp corrId queueId command <- (_, _, (corrId, queueId, Right command))
sendRecv :: Transport c => THandle c -> (ByteString, ByteString, ByteString, ByteString) -> IO SignedTransmissionOrError
sendRecv h (sgn, corrId, qId, cmd) = tPutRaw h (sgn, corrId, encode qId, cmd) >> tGet fromServer h
pattern Ids :: RecipientId -> SenderId -> RcvPublicDhKey -> BrokerMsg
pattern Ids rId sId srvDh <- IDS (QIK rId sId srvDh)
signSendRecv :: Transport c => THandle c -> C.SafePrivateKey -> (ByteString, ByteString, ByteString) -> IO SignedTransmissionOrError
signSendRecv h pk (corrId, qId, cmd) = do
let t = B.intercalate " " [corrId, encode qId, cmd]
sendRecv :: forall c p. (Transport c, PartyI p) => THandle c -> (Maybe C.ASignature, ByteString, ByteString, Command p) -> IO (SignedTransmission BrokerMsg)
sendRecv h@THandle {sessionId} (sgn, corrId, qId, cmd) = do
let t = encodeTransmission sessionId (CorrId corrId, qId, cmd)
Right () <- tPut h (sgn, t)
tGet h
signSendRecv :: forall c p. (Transport c, PartyI p) => THandle c -> C.APrivateSignKey -> (ByteString, ByteString, Command p) -> IO (SignedTransmission BrokerMsg)
signSendRecv h@THandle {sessionId} pk (corrId, qId, cmd) = do
let t = encodeTransmission sessionId (CorrId corrId, qId, cmd)
Right sig <- runExceptT $ C.sign pk t
_ <- tPut h (sig, t)
tGet fromServer h
cmdSEND :: ByteString -> ByteString
cmdSEND msg = serializeCommand (Cmd SSender . SEND $ msg)
Right () <- tPut h (Just sig, t)
tGet h
(#==) :: (HasCallStack, Eq a, Show a) => (a, a) -> String -> Assertion
(actual, expected) #== message = assertEqual message expected actual
@@ -63,353 +68,449 @@ testCreateSecure :: ATransport -> Spec
testCreateSecure (ATransport t) =
it "should create (NEW) and secure (KEY) queue" $
smpTest t $ \h -> do
(rPub, rKey) <- C.generateKeyPair rsaKeySize
Resp "abcd" rId1 (IDS rId sId) <- signSendRecv h rKey ("abcd", "", "NEW " <> C.serializePubKey rPub)
(rPub, rKey) <- C.generateSignatureKeyPair C.SEd448
(dhPub, dhPriv :: C.PrivateKeyX25519) <- C.generateKeyPair'
Resp "abcd" rId1 (Ids rId sId srvDh) <- signSendRecv h rKey ("abcd", "", NEW rPub dhPub)
let dec nonce = C.cbDecrypt (C.dh' srvDh dhPriv) (C.cbNonce nonce)
(rId1, "") #== "creates queue"
Resp "bcda" sId1 ok1 <- sendRecv h ("", "bcda", sId, "SEND 5 hello ")
Resp "bcda" sId1 ok1 <- sendRecv h ("", "bcda", sId, SEND "hello")
(ok1, OK) #== "accepts unsigned SEND"
(sId1, sId) #== "same queue ID in response 1"
Resp "" _ (MSG _ _ msg1) <- tGet fromServer h
(msg1, "hello") #== "delivers message"
Resp "" _ (MSG mId1 _ msg1) <- tGet h
(dec mId1 msg1, Right "hello") #== "delivers message"
Resp "cdab" _ ok4 <- signSendRecv h rKey ("cdab", rId, "ACK")
Resp "cdab" _ ok4 <- signSendRecv h rKey ("cdab", rId, ACK)
(ok4, OK) #== "replies OK when message acknowledged if no more messages"
Resp "dabc" _ err6 <- signSendRecv h rKey ("dabc", rId, "ACK")
Resp "dabc" _ err6 <- signSendRecv h rKey ("dabc", rId, ACK)
(err6, ERR NO_MSG) #== "replies ERR when message acknowledged without messages"
(sPub, sKey) <- C.generateKeyPair rsaKeySize
Resp "abcd" sId2 err1 <- signSendRecv h sKey ("abcd", sId, "SEND 5 hello ")
(sPub, sKey) <- C.generateSignatureKeyPair C.SEd448
Resp "abcd" sId2 err1 <- signSendRecv h sKey ("abcd", sId, SEND "hello")
(err1, ERR AUTH) #== "rejects signed SEND"
(sId2, sId) #== "same queue ID in response 2"
let keyCmd = "KEY " <> C.serializePubKey sPub
Resp "bcda" _ err2 <- sendRecv h (sampleSig, "bcda", rId, keyCmd)
Resp "bcda" _ err2 <- sendRecv h (sampleSig, "bcda", rId, KEY sPub)
(err2, ERR AUTH) #== "rejects KEY with wrong signature"
Resp "cdab" _ err3 <- signSendRecv h rKey ("cdab", sId, keyCmd)
Resp "cdab" _ err3 <- signSendRecv h rKey ("cdab", sId, KEY sPub)
(err3, ERR AUTH) #== "rejects KEY with sender's ID"
Resp "dabc" rId2 ok2 <- signSendRecv h rKey ("dabc", rId, keyCmd)
Resp "dabc" rId2 ok2 <- signSendRecv h rKey ("dabc", rId, KEY sPub)
(ok2, OK) #== "secures queue"
(rId2, rId) #== "same queue ID in response 3"
Resp "abcd" _ err4 <- signSendRecv h rKey ("abcd", rId, keyCmd)
Resp "abcd" _ err4 <- signSendRecv h rKey ("abcd", rId, KEY sPub)
(err4, ERR AUTH) #== "rejects KEY if already secured"
Resp "bcda" _ ok3 <- signSendRecv h sKey ("bcda", sId, "SEND 11 hello again ")
Resp "bcda" _ ok3 <- signSendRecv h sKey ("bcda", sId, SEND "hello again")
(ok3, OK) #== "accepts signed SEND"
Resp "" _ (MSG _ _ msg) <- tGet fromServer h
(msg, "hello again") #== "delivers message 2"
Resp "" _ (MSG mId2 _ msg2) <- tGet h
(dec mId2 msg2, Right "hello again") #== "delivers message 2"
Resp "cdab" _ ok5 <- signSendRecv h rKey ("cdab", rId, "ACK")
Resp "cdab" _ ok5 <- signSendRecv h rKey ("cdab", rId, ACK)
(ok5, OK) #== "replies OK when message acknowledged 2"
Resp "dabc" _ err5 <- sendRecv h ("", "dabc", sId, "SEND 5 hello ")
Resp "dabc" _ err5 <- sendRecv h ("", "dabc", sId, SEND "hello")
(err5, ERR AUTH) #== "rejects unsigned SEND"
testCreateDelete :: ATransport -> Spec
testCreateDelete (ATransport t) =
it "should create (NEW), suspend (OFF) and delete (DEL) queue" $
smpTest2 t $ \rh sh -> do
(rPub, rKey) <- C.generateKeyPair rsaKeySize
Resp "abcd" rId1 (IDS rId sId) <- signSendRecv rh rKey ("abcd", "", "NEW " <> C.serializePubKey rPub)
(rPub, rKey) <- C.generateSignatureKeyPair C.SEd25519
(dhPub, dhPriv :: C.PrivateKeyX25519) <- C.generateKeyPair'
Resp "abcd" rId1 (Ids rId sId srvDh) <- signSendRecv rh rKey ("abcd", "", NEW rPub dhPub)
let dec nonce = C.cbDecrypt (C.dh' srvDh dhPriv) (C.cbNonce nonce)
(rId1, "") #== "creates queue"
(sPub, sKey) <- C.generateKeyPair rsaKeySize
Resp "bcda" _ ok1 <- signSendRecv rh rKey ("bcda", rId, "KEY " <> C.serializePubKey sPub)
(sPub, sKey) <- C.generateSignatureKeyPair C.SEd25519
Resp "bcda" _ ok1 <- signSendRecv rh rKey ("bcda", rId, KEY sPub)
(ok1, OK) #== "secures queue"
Resp "cdab" _ ok2 <- signSendRecv sh sKey ("cdab", sId, "SEND 5 hello ")
Resp "cdab" _ ok2 <- signSendRecv sh sKey ("cdab", sId, SEND "hello")
(ok2, OK) #== "accepts signed SEND"
Resp "dabc" _ ok7 <- signSendRecv sh sKey ("dabc", sId, "SEND 7 hello 2 ")
Resp "dabc" _ ok7 <- signSendRecv sh sKey ("dabc", sId, SEND "hello 2")
(ok7, OK) #== "accepts signed SEND 2 - this message is not delivered because the first is not ACKed"
Resp "" _ (MSG _ _ msg1) <- tGet fromServer rh
(msg1, "hello") #== "delivers message"
Resp "" _ (MSG mId1 _ msg1) <- tGet rh
(dec mId1 msg1, Right "hello") #== "delivers message"
Resp "abcd" _ err1 <- sendRecv rh (sampleSig, "abcd", rId, "OFF")
Resp "abcd" _ err1 <- sendRecv rh (sampleSig, "abcd", rId, OFF)
(err1, ERR AUTH) #== "rejects OFF with wrong signature"
Resp "bcda" _ err2 <- signSendRecv rh rKey ("bcda", sId, "OFF")
Resp "bcda" _ err2 <- signSendRecv rh rKey ("bcda", sId, OFF)
(err2, ERR AUTH) #== "rejects OFF with sender's ID"
Resp "cdab" rId2 ok3 <- signSendRecv rh rKey ("cdab", rId, "OFF")
Resp "cdab" rId2 ok3 <- signSendRecv rh rKey ("cdab", rId, OFF)
(ok3, OK) #== "suspends queue"
(rId2, rId) #== "same queue ID in response 2"
Resp "dabc" _ err3 <- signSendRecv sh sKey ("dabc", sId, "SEND 5 hello ")
Resp "dabc" _ err3 <- signSendRecv sh sKey ("dabc", sId, SEND "hello")
(err3, ERR AUTH) #== "rejects signed SEND"
Resp "abcd" _ err4 <- sendRecv sh ("", "abcd", sId, "SEND 5 hello ")
Resp "abcd" _ err4 <- sendRecv sh ("", "abcd", sId, SEND "hello")
(err4, ERR AUTH) #== "reject unsigned SEND too"
Resp "bcda" _ ok4 <- signSendRecv rh rKey ("bcda", rId, "OFF")
Resp "bcda" _ ok4 <- signSendRecv rh rKey ("bcda", rId, OFF)
(ok4, OK) #== "accepts OFF when suspended"
Resp "cdab" _ (MSG _ _ msg) <- signSendRecv rh rKey ("cdab", rId, "SUB")
(msg, "hello") #== "accepts SUB when suspended and delivers the message again (because was not ACKed)"
Resp "cdab" _ (MSG mId2 _ msg2) <- signSendRecv rh rKey ("cdab", rId, SUB)
(dec mId2 msg2, Right "hello") #== "accepts SUB when suspended and delivers the message again (because was not ACKed)"
Resp "dabc" _ err5 <- sendRecv rh (sampleSig, "dabc", rId, "DEL")
Resp "dabc" _ err5 <- sendRecv rh (sampleSig, "dabc", rId, DEL)
(err5, ERR AUTH) #== "rejects DEL with wrong signature"
Resp "abcd" _ err6 <- signSendRecv rh rKey ("abcd", sId, "DEL")
Resp "abcd" _ err6 <- signSendRecv rh rKey ("abcd", sId, DEL)
(err6, ERR AUTH) #== "rejects DEL with sender's ID"
Resp "bcda" rId3 ok6 <- signSendRecv rh rKey ("bcda", rId, "DEL")
Resp "bcda" rId3 ok6 <- signSendRecv rh rKey ("bcda", rId, DEL)
(ok6, OK) #== "deletes queue"
(rId3, rId) #== "same queue ID in response 3"
Resp "cdab" _ err7 <- signSendRecv sh sKey ("cdab", sId, "SEND 5 hello ")
Resp "cdab" _ err7 <- signSendRecv sh sKey ("cdab", sId, SEND "hello")
(err7, ERR AUTH) #== "rejects signed SEND when deleted"
Resp "dabc" _ err8 <- sendRecv sh ("", "dabc", sId, "SEND 5 hello ")
Resp "dabc" _ err8 <- sendRecv sh ("", "dabc", sId, SEND "hello")
(err8, ERR AUTH) #== "rejects unsigned SEND too when deleted"
Resp "abcd" _ err11 <- signSendRecv rh rKey ("abcd", rId, "ACK")
Resp "abcd" _ err11 <- signSendRecv rh rKey ("abcd", rId, ACK)
(err11, ERR AUTH) #== "rejects ACK when conn deleted - the second message is deleted"
Resp "bcda" _ err9 <- signSendRecv rh rKey ("bcda", rId, "OFF")
Resp "bcda" _ err9 <- signSendRecv rh rKey ("bcda", rId, OFF)
(err9, ERR AUTH) #== "rejects OFF when deleted"
Resp "cdab" _ err10 <- signSendRecv rh rKey ("cdab", rId, "SUB")
Resp "cdab" _ err10 <- signSendRecv rh rKey ("cdab", rId, SUB)
(err10, ERR AUTH) #== "rejects SUB when deleted"
stressTest :: ATransport -> Spec
stressTest (ATransport t) =
it "should create many queues, disconnect and re-connect" $
smpTest3 t $ \h1 h2 h3 -> do
(rPub, rKey) <- C.generateSignatureKeyPair C.SEd25519
(dhPub, _ :: C.PrivateKeyX25519) <- C.generateKeyPair'
rIds <- forM [1 .. 50 :: Int] . const $ do
Resp "" "" (Ids rId _ _) <- signSendRecv h1 rKey ("", "", NEW rPub dhPub)
pure rId
let subscribeQueues h = forM_ rIds $ \rId -> do
Resp "" rId' OK <- signSendRecv h rKey ("", rId, SUB)
rId' `shouldBe` rId
closeConnection $ connection h1
subscribeQueues h2
closeConnection $ connection h2
subscribeQueues h3
testDuplex :: ATransport -> Spec
testDuplex (ATransport t) =
it "should create 2 simplex connections and exchange messages" $
smpTest2 t $ \alice bob -> do
(arPub, arKey) <- C.generateKeyPair rsaKeySize
Resp "abcd" _ (IDS aRcv aSnd) <- signSendRecv alice arKey ("abcd", "", "NEW " <> C.serializePubKey arPub)
(arPub, arKey) <- C.generateSignatureKeyPair C.SEd448
(aDhPub, aDhPriv :: C.PrivateKeyX25519) <- C.generateKeyPair'
Resp "abcd" _ (Ids aRcv aSnd aSrvDh) <- signSendRecv alice arKey ("abcd", "", NEW arPub aDhPub)
let aDec nonce = C.cbDecrypt (C.dh' aSrvDh aDhPriv) (C.cbNonce nonce)
-- aSnd ID is passed to Bob out-of-band
(bsPub, bsKey) <- C.generateKeyPair rsaKeySize
Resp "bcda" _ OK <- sendRecv bob ("", "bcda", aSnd, cmdSEND $ "key " <> C.serializePubKey bsPub)
-- "key ..." is ad-hoc, different from SMP protocol
(bsPub, bsKey) <- C.generateSignatureKeyPair C.SEd448
Resp "bcda" _ OK <- sendRecv bob ("", "bcda", aSnd, SEND $ "key " <> strEncode bsPub)
-- "key ..." is ad-hoc, not a part of SMP protocol
Resp "" _ (MSG _ _ msg1) <- tGet fromServer alice
Resp "cdab" _ OK <- signSendRecv alice arKey ("cdab", aRcv, "ACK")
["key", bobKey] <- return $ B.words msg1
(bobKey, C.serializePubKey bsPub) #== "key received from Bob"
Resp "dabc" _ OK <- signSendRecv alice arKey ("dabc", aRcv, "KEY " <> bobKey)
Resp "" _ (MSG mId1 _ msg1) <- tGet alice
Resp "cdab" _ OK <- signSendRecv alice arKey ("cdab", aRcv, ACK)
Right ["key", bobKey] <- pure $ B.words <$> aDec mId1 msg1
(bobKey, strEncode bsPub) #== "key received from Bob"
Resp "dabc" _ OK <- signSendRecv alice arKey ("dabc", aRcv, KEY bsPub)
(brPub, brKey) <- C.generateKeyPair rsaKeySize
Resp "abcd" _ (IDS bRcv bSnd) <- signSendRecv bob brKey ("abcd", "", "NEW " <> C.serializePubKey brPub)
Resp "bcda" _ OK <- signSendRecv bob bsKey ("bcda", aSnd, cmdSEND $ "reply_id " <> encode bSnd)
-- "reply_id ..." is ad-hoc, it is not a part of SMP protocol
(brPub, brKey) <- C.generateSignatureKeyPair C.SEd448
(bDhPub, bDhPriv :: C.PrivateKeyX25519) <- C.generateKeyPair'
Resp "abcd" _ (Ids bRcv bSnd bSrvDh) <- signSendRecv bob brKey ("abcd", "", NEW brPub bDhPub)
let bDec nonce = C.cbDecrypt (C.dh' bSrvDh bDhPriv) (C.cbNonce nonce)
Resp "bcda" _ OK <- signSendRecv bob bsKey ("bcda", aSnd, SEND $ "reply_id " <> encode bSnd)
-- "reply_id ..." is ad-hoc, not a part of SMP protocol
Resp "" _ (MSG _ _ msg2) <- tGet fromServer alice
Resp "cdab" _ OK <- signSendRecv alice arKey ("cdab", aRcv, "ACK")
["reply_id", bId] <- return $ B.words msg2
Resp "" _ (MSG mId2 _ msg2) <- tGet alice
Resp "cdab" _ OK <- signSendRecv alice arKey ("cdab", aRcv, ACK)
Right ["reply_id", bId] <- pure $ B.words <$> aDec mId2 msg2
(bId, encode bSnd) #== "reply queue ID received from Bob"
(asPub, asKey) <- C.generateKeyPair rsaKeySize
Resp "dabc" _ OK <- sendRecv alice ("", "dabc", bSnd, cmdSEND $ "key " <> C.serializePubKey asPub)
-- "key ..." is ad-hoc, different from SMP protocol
(asPub, asKey) <- C.generateSignatureKeyPair C.SEd448
Resp "dabc" _ OK <- sendRecv alice ("", "dabc", bSnd, SEND $ "key " <> strEncode asPub)
-- "key ..." is ad-hoc, not a part of SMP protocol
Resp "" _ (MSG _ _ msg3) <- tGet fromServer bob
Resp "abcd" _ OK <- signSendRecv bob brKey ("abcd", bRcv, "ACK")
["key", aliceKey] <- return $ B.words msg3
(aliceKey, C.serializePubKey asPub) #== "key received from Alice"
Resp "bcda" _ OK <- signSendRecv bob brKey ("bcda", bRcv, "KEY " <> aliceKey)
Resp "" _ (MSG mId3 _ msg3) <- tGet bob
Resp "abcd" _ OK <- signSendRecv bob brKey ("abcd", bRcv, ACK)
Right ["key", aliceKey] <- pure $ B.words <$> bDec mId3 msg3
(aliceKey, strEncode asPub) #== "key received from Alice"
Resp "bcda" _ OK <- signSendRecv bob brKey ("bcda", bRcv, KEY asPub)
Resp "cdab" _ OK <- signSendRecv bob bsKey ("cdab", aSnd, "SEND 8 hi alice ")
Resp "cdab" _ OK <- signSendRecv bob bsKey ("cdab", aSnd, SEND "hi alice")
Resp "" _ (MSG _ _ msg4) <- tGet fromServer alice
Resp "dabc" _ OK <- signSendRecv alice arKey ("dabc", aRcv, "ACK")
(msg4, "hi alice") #== "message received from Bob"
Resp "" _ (MSG mId4 _ msg4) <- tGet alice
Resp "dabc" _ OK <- signSendRecv alice arKey ("dabc", aRcv, ACK)
(aDec mId4 msg4, Right "hi alice") #== "message received from Bob"
Resp "abcd" _ OK <- signSendRecv alice asKey ("abcd", bSnd, cmdSEND "how are you bob")
Resp "abcd" _ OK <- signSendRecv alice asKey ("abcd", bSnd, SEND "how are you bob")
Resp "" _ (MSG _ _ msg5) <- tGet fromServer bob
Resp "bcda" _ OK <- signSendRecv bob brKey ("bcda", bRcv, "ACK")
(msg5, "how are you bob") #== "message received from alice"
Resp "" _ (MSG mId5 _ msg5) <- tGet bob
Resp "bcda" _ OK <- signSendRecv bob brKey ("bcda", bRcv, ACK)
(bDec mId5 msg5, Right "how are you bob") #== "message received from alice"
testSwitchSub :: ATransport -> Spec
testSwitchSub (ATransport t) =
it "should create simplex connections and switch subscription to another TCP connection" $
smpTest3 t $ \rh1 rh2 sh -> do
(rPub, rKey) <- C.generateKeyPair rsaKeySize
Resp "abcd" _ (IDS rId sId) <- signSendRecv rh1 rKey ("abcd", "", "NEW " <> C.serializePubKey rPub)
Resp "bcda" _ ok1 <- sendRecv sh ("", "bcda", sId, "SEND 5 test1 ")
(rPub, rKey) <- C.generateSignatureKeyPair C.SEd448
(dhPub, dhPriv :: C.PrivateKeyX25519) <- C.generateKeyPair'
Resp "abcd" _ (Ids rId sId srvDh) <- signSendRecv rh1 rKey ("abcd", "", NEW rPub dhPub)
let dec nonce = C.cbDecrypt (C.dh' srvDh dhPriv) (C.cbNonce nonce)
Resp "bcda" _ ok1 <- sendRecv sh ("", "bcda", sId, SEND "test1")
(ok1, OK) #== "sent test message 1"
Resp "cdab" _ ok2 <- sendRecv sh ("", "cdab", sId, cmdSEND "test2, no ACK")
Resp "cdab" _ ok2 <- sendRecv sh ("", "cdab", sId, SEND "test2, no ACK")
(ok2, OK) #== "sent test message 2"
Resp "" _ (MSG _ _ msg1) <- tGet fromServer rh1
(msg1, "test1") #== "test message 1 delivered to the 1st TCP connection"
Resp "abcd" _ (MSG _ _ msg2) <- signSendRecv rh1 rKey ("abcd", rId, "ACK")
(msg2, "test2, no ACK") #== "test message 2 delivered, no ACK"
Resp "" _ (MSG mId1 _ msg1) <- tGet rh1
(dec mId1 msg1, Right "test1") #== "test message 1 delivered to the 1st TCP connection"
Resp "abcd" _ (MSG mId2 _ msg2) <- signSendRecv rh1 rKey ("abcd", rId, ACK)
(dec mId2 msg2, Right "test2, no ACK") #== "test message 2 delivered, no ACK"
Resp "bcda" _ (MSG _ _ msg2') <- signSendRecv rh2 rKey ("bcda", rId, "SUB")
(msg2', "test2, no ACK") #== "same simplex queue via another TCP connection, tes2 delivered again (no ACK in 1st queue)"
Resp "cdab" _ OK <- signSendRecv rh2 rKey ("cdab", rId, "ACK")
Resp "bcda" _ (MSG mId2' _ msg2') <- signSendRecv rh2 rKey ("bcda", rId, SUB)
(dec mId2' msg2', Right "test2, no ACK") #== "same simplex queue via another TCP connection, tes2 delivered again (no ACK in 1st queue)"
Resp "cdab" _ OK <- signSendRecv rh2 rKey ("cdab", rId, ACK)
Resp "" _ end <- tGet fromServer rh1
Resp "" _ end <- tGet rh1
(end, END) #== "unsubscribed the 1st TCP connection"
Resp "dabc" _ OK <- sendRecv sh ("", "dabc", sId, "SEND 5 test3 ")
Resp "dabc" _ OK <- sendRecv sh ("", "dabc", sId, SEND "test3")
Resp "" _ (MSG _ _ msg3) <- tGet fromServer rh2
(msg3, "test3") #== "delivered to the 2nd TCP connection"
Resp "" _ (MSG mId3 _ msg3) <- tGet rh2
(dec mId3 msg3, Right "test3") #== "delivered to the 2nd TCP connection"
Resp "abcd" _ err <- signSendRecv rh1 rKey ("abcd", rId, "ACK")
Resp "abcd" _ err <- signSendRecv rh1 rKey ("abcd", rId, ACK)
(err, ERR NO_MSG) #== "rejects ACK from the 1st TCP connection"
Resp "bcda" _ ok3 <- signSendRecv rh2 rKey ("bcda", rId, "ACK")
Resp "bcda" _ ok3 <- signSendRecv rh2 rKey ("bcda", rId, ACK)
(ok3, OK) #== "accepts ACK from the 2nd TCP connection"
1000 `timeout` tGet fromServer rh1 >>= \case
1000 `timeout` tGet @BrokerMsg rh1 >>= \case
Nothing -> return ()
Just _ -> error "nothing else is delivered to the 1st TCP connection"
testWithStoreLog :: ATransport -> Spec
testWithStoreLog at@(ATransport t) =
it "should store simplex queues to log and restore them after server restart" $ do
(sPub1, sKey1) <- C.generateKeyPair rsaKeySize
(sPub2, sKey2) <- C.generateKeyPair rsaKeySize
(sPub1, sKey1) <- C.generateSignatureKeyPair C.SEd25519
(sPub2, sKey2) <- C.generateSignatureKeyPair C.SEd25519
(nPub, nKey) <- C.generateSignatureKeyPair C.SEd25519
recipientId1 <- newTVarIO ""
recipientKey1 <- newTVarIO Nothing
dhShared1 <- newTVarIO Nothing
senderId1 <- newTVarIO ""
senderId2 <- newTVarIO ""
notifierId <- newTVarIO ""
withSmpServerStoreLogOn at testPort . runTest t $ \h -> do
(sId1, _, _) <- createAndSecureQueue h sPub1
atomically $ writeTVar senderId1 sId1
Resp "bcda" _ OK <- signSendRecv h sKey1 ("bcda", sId1, "SEND 5 hello ")
Resp "" _ (MSG _ _ "hello") <- tGet fromServer h
withSmpServerStoreLogOn at testPort . runTest t $ \h -> runClient t $ \h1 -> do
(sId1, rId1, rKey1, dhShared) <- createAndSecureQueue h sPub1
Resp "abcd" _ (NID nId) <- signSendRecv h rKey1 ("abcd", rId1, NKEY nPub)
atomically $ do
writeTVar recipientId1 rId1
writeTVar recipientKey1 $ Just rKey1
writeTVar dhShared1 $ Just dhShared
writeTVar senderId1 sId1
writeTVar notifierId nId
Resp "dabc" _ OK <- signSendRecv h1 nKey ("dabc", nId, NSUB)
Resp "bcda" _ OK <- signSendRecv h sKey1 ("bcda", sId1, SEND "hello")
Resp "" _ (MSG mId1 _ msg1) <- tGet h
(C.cbDecrypt dhShared (C.cbNonce mId1) msg1, Right "hello") #== "delivered from queue 1"
Resp "" _ NMSG <- tGet h1
(sId2, rId2, rKey2) <- createAndSecureQueue h sPub2
(sId2, rId2, rKey2, dhShared2) <- createAndSecureQueue h sPub2
atomically $ writeTVar senderId2 sId2
Resp "cdab" _ OK <- signSendRecv h sKey2 ("cdab", sId2, "SEND 9 hello too ")
Resp "" _ (MSG _ _ "hello too") <- tGet fromServer h
Resp "cdab" _ OK <- signSendRecv h sKey2 ("cdab", sId2, SEND "hello too")
Resp "" _ (MSG mId2 _ msg2) <- tGet h
(C.cbDecrypt dhShared2 (C.cbNonce mId2) msg2, Right "hello too") #== "delivered from queue 2"
Resp "dabc" _ OK <- signSendRecv h rKey2 ("dabc", rId2, "DEL")
Resp "dabc" _ OK <- signSendRecv h rKey2 ("dabc", rId2, DEL)
pure ()
logSize `shouldReturn` 5
logSize `shouldReturn` 6
withSmpServerThreadOn at testPort . runTest t $ \h -> do
sId1 <- readTVarIO senderId1
-- fails if store log is disabled
Resp "bcda" _ (ERR AUTH) <- signSendRecv h sKey1 ("bcda", sId1, "SEND 5 hello ")
Resp "bcda" _ (ERR AUTH) <- signSendRecv h sKey1 ("bcda", sId1, SEND "hello")
pure ()
withSmpServerStoreLogOn at testPort . runTest t $ \h -> do
withSmpServerStoreLogOn at testPort . runTest t $ \h -> runClient t $ \h1 -> do
-- this queue is restored
rId1 <- readTVarIO recipientId1
Just rKey1 <- readTVarIO recipientKey1
Just dh1 <- readTVarIO dhShared1
sId1 <- readTVarIO senderId1
Resp "bcda" _ OK <- signSendRecv h sKey1 ("bcda", sId1, "SEND 5 hello ")
nId <- readTVarIO notifierId
Resp "dabc" _ OK <- signSendRecv h1 nKey ("dabc", nId, NSUB)
Resp "bcda" _ OK <- signSendRecv h sKey1 ("bcda", sId1, SEND "hello")
Resp "cdab" _ (MSG mId3 _ msg3) <- signSendRecv h rKey1 ("cdab", rId1, SUB)
(C.cbDecrypt dh1 (C.cbNonce mId3) msg3, Right "hello") #== "delivered from restored queue"
Resp "" _ NMSG <- tGet h1
-- this queue is removed - not restored
sId2 <- readTVarIO senderId2
Resp "cdab" _ (ERR AUTH) <- signSendRecv h sKey2 ("cdab", sId2, "SEND 9 hello too ")
Resp "cdab" _ (ERR AUTH) <- signSendRecv h sKey2 ("cdab", sId2, SEND "hello too")
pure ()
logSize `shouldReturn` 1
removeFile testStoreLogFile
where
createAndSecureQueue :: Transport c => THandle c -> SenderPublicKey -> IO (SenderId, RecipientId, C.SafePrivateKey)
createAndSecureQueue h sPub = do
(rPub, rKey) <- C.generateKeyPair rsaKeySize
Resp "abcd" "" (IDS rId sId) <- signSendRecv h rKey ("abcd", "", "NEW " <> C.serializePubKey rPub)
let keyCmd = "KEY " <> C.serializePubKey sPub
Resp "dabc" rId' OK <- signSendRecv h rKey ("dabc", rId, keyCmd)
(rId', rId) #== "same queue ID"
pure (sId, rId, rKey)
runTest :: Transport c => TProxy c -> (THandle c -> IO ()) -> ThreadId -> Expectation
runTest _ test' server = do
testSMPClient test' `shouldReturn` ()
killThread server
runClient :: Transport c => TProxy c -> (THandle c -> IO ()) -> Expectation
runClient _ test' = testSMPClient test' `shouldReturn` ()
logSize :: IO Int
logSize =
try (length . B.lines <$> B.readFile testStoreLogFile) >>= \case
Right l -> pure l
Left (_ :: SomeException) -> logSize
createAndSecureQueue :: Transport c => THandle c -> SndPublicVerifyKey -> IO (SenderId, RecipientId, RcvPrivateSignKey, RcvDhSecret)
createAndSecureQueue h sPub = do
(rPub, rKey) <- C.generateSignatureKeyPair C.SEd448
(dhPub, dhPriv :: C.PrivateKeyX25519) <- C.generateKeyPair'
Resp "abcd" "" (Ids rId sId srvDh) <- signSendRecv h rKey ("abcd", "", NEW rPub dhPub)
let dhShared = C.dh' srvDh dhPriv
Resp "dabc" rId' OK <- signSendRecv h rKey ("dabc", rId, KEY sPub)
(rId', rId) #== "same queue ID"
pure (sId, rId, rKey, dhShared)
testTiming :: ATransport -> Spec
testTiming (ATransport t) =
it "should have similar time for auth error, whether queue exists or not, for all key sizes" $
smpTest2 t $ \rh sh ->
mapM_
(testSameTiming rh sh)
[ (128, 128, 100),
(128, 256, 25),
-- (128, 512, 15),
(256, 128, 100),
(256, 256, 25)
-- (256, 512, 15),
-- (512, 128, 100),
-- (512, 256, 25),
-- (512, 512, 15)
[ (32, 32, 200),
(32, 57, 100),
(57, 32, 200),
(57, 57, 100)
]
where
timeRepeat n = fmap fst . timeItT . forM_ (replicate n ()) . const
similarTime t1 t2 = abs (t1 - t2) / t1 < 0.2 `shouldBe` True
similarTime t1 t2 = abs (t2 / t1 - 1) < 0.25 `shouldBe` True
testSameTiming :: Transport c => THandle c -> THandle c -> (Int, Int, Int) -> Expectation
testSameTiming rh sh (senderKeySize, badKeySize, n) = do
(rPub, rKey) <- C.generateKeyPair rsaKeySize
Resp "abcd" "" (IDS rId sId) <- signSendRecv rh rKey ("abcd", "", "NEW " <> C.serializePubKey rPub)
testSameTiming rh sh (goodKeySize, badKeySize, n) = do
(rPub, rKey) <- generateKeys goodKeySize
(dhPub, dhPriv :: C.PrivateKeyX25519) <- C.generateKeyPair'
Resp "abcd" "" (Ids rId sId srvDh) <- signSendRecv rh rKey ("abcd", "", NEW rPub dhPub)
let dec nonce = C.cbDecrypt (C.dh' srvDh dhPriv) (C.cbNonce nonce)
Resp "cdab" _ OK <- signSendRecv rh rKey ("cdab", rId, SUB)
(sPub, sKey) <- C.generateKeyPair senderKeySize
let keyCmd = "KEY " <> C.serializePubKey sPub
Resp "dabc" _ OK <- signSendRecv rh rKey ("dabc", rId, keyCmd)
(_, badKey) <- generateKeys badKeySize
-- runTimingTest rh badKey rId "SUB"
(_, badKey) <- C.generateKeyPair badKeySize
Resp "bcda" _ OK <- signSendRecv sh sKey ("bcda", sId, "SEND 5 hello ")
timeWrongKey <- timeRepeat n $ do
Resp "cdab" _ (ERR AUTH) <- signSendRecv sh badKey ("cdab", sId, "SEND 5 hello ")
return ()
timeNoQueue <- timeRepeat n $ do
Resp "dabc" _ (ERR AUTH) <- signSendRecv sh badKey ("dabc", "1234", "SEND 5 hello ")
return ()
Resp "" _ (MSG _ _ "hello") <- tGet fromServer rh
similarTime timeNoQueue timeWrongKey
(sPub, sKey) <- generateKeys goodKeySize
Resp "dabc" _ OK <- signSendRecv rh rKey ("dabc", rId, KEY sPub)
samplePubKey :: ByteString
samplePubKey = "rsa:MIIBoDANBgkqhkiG9w0BAQEFAAOCAY0AMIIBiAKCAQEAtn1NI2tPoOGSGfad0aUg0tJ0kG2nzrIPGLiz8wb3dQSJC9xkRHyzHhEE8Kmy2cM4q7rNZIlLcm4M7oXOTe7SC4x59bLQG9bteZPKqXu9wk41hNamV25PWQ4zIcIRmZKETVGbwN7jFMpH7wxLdI1zzMArAPKXCDCJ5ctWh4OWDI6OR6AcCtEj+toCI6N6pjxxn5VigJtwiKhxYpoUJSdNM60wVEDCSUrZYBAuDH8pOxPfP+Tm4sokaFDTIG3QJFzOjC+/9nW4MUjAOFll9PCp9kaEFHJ/YmOYKMWNOCCPvLS6lxA83i0UaardkNLNoFS5paWfTlroxRwOC2T6PwO2ywKBgDjtXcSED61zK1seocQMyGRINnlWdhceD669kIHju/f6kAayvYKW3/lbJNXCmyinAccBosO08/0sUxvtuniIo18kfYJE0UmP1ReCjhMP+O+yOmwZJini/QelJk/Pez8IIDDWnY1qYQsN/q7ocjakOYrpGG7mig6JMFpDJtD6istR"
Resp "bcda" _ OK <- signSendRecv sh sKey ("bcda", sId, SEND "hello")
Resp "" _ (MSG mId _ msg) <- tGet rh
(dec mId msg, Right "hello") #== "delivered from queue"
sampleSig :: ByteString
sampleSig = "\128\207*\159eq\220i!\"\157\161\130\184\226\246\232_\\\170`\180\160\230sI\154\197\211\252\SUB\246\206ELL\t9K\ESC\196?\128\215%\222\148\NAK;9\155f\164\217e\242\156\CAN9\253\r\170\174'w\211\228?\205)\215\150\255\247z\DC115\DC1{\bn\145\rKD,K\230\202d8\233\167|7y\t_S\EM\248\EOT\216\172\167d\181\224)\137\ACKo\197j#c\217\243\228.\167\228\205\144\vr\134"
runTimingTest sh badKey sId $ SEND "hello"
where
generateKeys = \case
32 -> C.generateSignatureKeyPair C.SEd25519
57 -> C.generateSignatureKeyPair C.SEd448
_ -> error "unsupported key size"
runTimingTest h badKey qId cmd = do
timeWrongKey <- timeRepeat n $ do
Resp "cdab" _ (ERR AUTH) <- signSendRecv h badKey ("cdab", qId, cmd)
return ()
timeNoQueue <- timeRepeat n $ do
Resp "dabc" _ (ERR AUTH) <- signSendRecv h badKey ("dabc", "1234", cmd)
return ()
-- (putStrLn . unwords . map show)
-- [ fromIntegral goodKeySize,
-- fromIntegral badKeySize,
-- timeWrongKey,
-- timeNoQueue,
-- timeWrongKey / timeNoQueue - 1
-- ]
similarTime timeNoQueue timeWrongKey
testMessageNotifications :: ATransport -> Spec
testMessageNotifications (ATransport t) =
it "should create simplex connection, subscribe notifier and deliver notifications" $ do
(sPub, sKey) <- C.generateSignatureKeyPair C.SEd25519
(nPub, nKey) <- C.generateSignatureKeyPair C.SEd25519
smpTest4 t $ \rh sh nh1 nh2 -> do
(sId, rId, rKey, dhShared) <- createAndSecureQueue rh sPub
let dec nonce = C.cbDecrypt dhShared (C.cbNonce nonce)
Resp "1" _ (NID nId) <- signSendRecv rh rKey ("1", rId, NKEY nPub)
Resp "2" _ OK <- signSendRecv nh1 nKey ("2", nId, NSUB)
Resp "3" _ OK <- signSendRecv sh sKey ("3", sId, SEND "hello")
Resp "" _ (MSG mId1 _ msg1) <- tGet rh
(dec mId1 msg1, Right "hello") #== "delivered from queue"
Resp "3a" _ OK <- signSendRecv rh rKey ("3a", rId, ACK)
Resp "" _ NMSG <- tGet nh1
Resp "4" _ OK <- signSendRecv nh2 nKey ("4", nId, NSUB)
Resp "" _ END <- tGet nh1
Resp "5" _ OK <- signSendRecv sh sKey ("5", sId, SEND "hello again")
Resp "" _ (MSG mId2 _ msg2) <- tGet rh
(dec mId2 msg2, Right "hello again") #== "delivered from queue again"
Resp "" _ NMSG <- tGet nh2
1000 `timeout` tGet @BrokerMsg nh1 >>= \case
Nothing -> return ()
Just _ -> error "nothing else should be delivered to the 1st notifier's TCP connection"
samplePubKey :: C.APublicVerifyKey
samplePubKey = C.APublicVerifyKey C.SEd25519 "MCowBQYDK2VwAyEAfAOflyvbJv1fszgzkQ6buiZJVgSpQWsucXq7U6zjMgY="
sampleDhPubKey :: C.PublicKey 'C.X25519
sampleDhPubKey = "MCowBQYDK2VuAyEAriy+HcARIhqsgSjVnjKqoft+y6pxrxdY68zn4+LjYhQ="
sampleSig :: Maybe C.ASignature
sampleSig = "e8JK+8V3fq6kOLqco/SaKlpNaQ7i1gfOrXoqekEl42u4mF8Bgu14T5j0189CGcUhJHw2RwCMvON+qbvQ9ecJAA=="
syntaxTests :: ATransport -> Spec
syntaxTests (ATransport t) = do
it "unknown command" $ ("", "abcd", "1234", "HELLO") >#> ("", "abcd", "1234", "ERR CMD SYNTAX")
describe "NEW" do
it "no parameters" $ (sampleSig, "bcda", "", "NEW") >#> ("", "bcda", "", "ERR CMD SYNTAX")
it "many parameters" $ (sampleSig, "cdab", "", "NEW 1 " <> samplePubKey) >#> ("", "cdab", "", "ERR CMD SYNTAX")
it "no signature" $ ("", "dabc", "", "NEW " <> samplePubKey) >#> ("", "dabc", "", "ERR CMD NO_AUTH")
it "queue ID" $ (sampleSig, "abcd", "12345678", "NEW " <> samplePubKey) >#> ("", "abcd", "12345678", "ERR CMD HAS_AUTH")
describe "KEY" do
it "valid syntax" $ (sampleSig, "bcda", "12345678", "KEY " <> samplePubKey) >#> ("", "bcda", "12345678", "ERR AUTH")
it "no parameters" $ (sampleSig, "cdab", "12345678", "KEY") >#> ("", "cdab", "12345678", "ERR CMD SYNTAX")
it "many parameters" $ (sampleSig, "dabc", "12345678", "KEY 1 " <> samplePubKey) >#> ("", "dabc", "12345678", "ERR CMD SYNTAX")
it "no signature" $ ("", "abcd", "12345678", "KEY " <> samplePubKey) >#> ("", "abcd", "12345678", "ERR CMD NO_AUTH")
it "no queue ID" $ (sampleSig, "bcda", "", "KEY " <> samplePubKey) >#> ("", "bcda", "", "ERR CMD NO_AUTH")
noParamsSyntaxTest "SUB"
noParamsSyntaxTest "ACK"
noParamsSyntaxTest "OFF"
noParamsSyntaxTest "DEL"
describe "SEND" do
it "valid syntax 1" $ (sampleSig, "cdab", "12345678", "SEND 5 hello ") >#> ("", "cdab", "12345678", "ERR AUTH")
it "valid syntax 2" $ (sampleSig, "dabc", "12345678", "SEND 11 hello there ") >#> ("", "dabc", "12345678", "ERR AUTH")
it "no parameters" $ (sampleSig, "abcd", "12345678", "SEND") >#> ("", "abcd", "12345678", "ERR CMD SYNTAX")
it "no queue ID" $ (sampleSig, "bcda", "", "SEND 5 hello ") >#> ("", "bcda", "", "ERR CMD NO_QUEUE")
it "bad message body 1" $ (sampleSig, "cdab", "12345678", "SEND 11 hello ") >#> ("", "cdab", "12345678", "ERR CMD SYNTAX")
it "bad message body 2" $ (sampleSig, "dabc", "12345678", "SEND hello ") >#> ("", "dabc", "12345678", "ERR CMD SYNTAX")
it "bigger body" $ (sampleSig, "abcd", "12345678", "SEND 4 hello ") >#> ("", "abcd", "12345678", "ERR CMD SYNTAX")
describe "PING" do
it "valid syntax" $ ("", "abcd", "", "PING") >#> ("", "abcd", "", "PONG")
describe "broker response not allowed" do
it "OK" $ (sampleSig, "bcda", "12345678", "OK") >#> ("", "bcda", "12345678", "ERR CMD PROHIBITED")
it "unknown command" $ ("", "abcd", "1234", ('H', 'E', 'L', 'L', 'O')) >#> ("", "abcd", "1234", ERR $ CMD UNKNOWN)
describe "NEW" $ do
it "no parameters" $ (sampleSig, "bcda", "", NEW_) >#> ("", "bcda", "", ERR $ CMD SYNTAX)
it "many parameters" $ (sampleSig, "cdab", "", (NEW_, ' ', ('\x01', 'A'), samplePubKey, sampleDhPubKey)) >#> ("", "cdab", "", ERR $ CMD SYNTAX)
it "no signature" $ ("", "dabc", "", (NEW_, ' ', samplePubKey, sampleDhPubKey)) >#> ("", "dabc", "", ERR $ CMD NO_AUTH)
it "queue ID" $ (sampleSig, "abcd", "12345678", (NEW_, ' ', samplePubKey, sampleDhPubKey)) >#> ("", "abcd", "12345678", ERR $ CMD HAS_AUTH)
describe "KEY" $ do
it "valid syntax" $ (sampleSig, "bcda", "12345678", (KEY_, ' ', samplePubKey)) >#> ("", "bcda", "12345678", ERR AUTH)
it "no parameters" $ (sampleSig, "cdab", "12345678", KEY_) >#> ("", "cdab", "12345678", ERR $ CMD SYNTAX)
it "many parameters" $ (sampleSig, "dabc", "12345678", (KEY_, ' ', ('\x01', 'A'), samplePubKey)) >#> ("", "dabc", "12345678", ERR $ CMD SYNTAX)
it "no signature" $ ("", "abcd", "12345678", (KEY_, ' ', samplePubKey)) >#> ("", "abcd", "12345678", ERR $ CMD NO_AUTH)
it "no queue ID" $ (sampleSig, "bcda", "", (KEY_, ' ', samplePubKey)) >#> ("", "bcda", "", ERR $ CMD NO_AUTH)
noParamsSyntaxTest "SUB" SUB_
noParamsSyntaxTest "ACK" ACK_
noParamsSyntaxTest "OFF" OFF_
noParamsSyntaxTest "DEL" DEL_
describe "SEND" $ do
it "valid syntax" $ (sampleSig, "cdab", "12345678", (SEND_, ' ', "hello" :: ByteString)) >#> ("", "cdab", "12345678", ERR AUTH)
it "no parameters" $ (sampleSig, "abcd", "12345678", SEND_) >#> ("", "abcd", "12345678", ERR $ CMD SYNTAX)
it "no queue ID" $ (sampleSig, "bcda", "", (SEND_, ' ', "hello" :: ByteString)) >#> ("", "bcda", "", ERR $ CMD NO_QUEUE)
describe "PING" $ do
it "valid syntax" $ ("", "abcd", "", PING_) >#> ("", "abcd", "", PONG)
describe "broker response not allowed" $ do
it "OK" $ (sampleSig, "bcda", "12345678", OK_) >#> ("", "bcda", "12345678", ERR $ CMD UNKNOWN)
where
noParamsSyntaxTest :: ByteString -> Spec
noParamsSyntaxTest cmd = describe (B.unpack cmd) do
it "valid syntax" $ (sampleSig, "abcd", "12345678", cmd) >#> ("", "abcd", "12345678", "ERR AUTH")
it "wrong terminator" $ (sampleSig, "bcda", "12345678", cmd <> "=") >#> ("", "bcda", "12345678", "ERR CMD SYNTAX")
it "no signature" $ ("", "cdab", "12345678", cmd) >#> ("", "cdab", "12345678", "ERR CMD NO_AUTH")
it "no queue ID" $ (sampleSig, "dabc", "", cmd) >#> ("", "dabc", "", "ERR CMD NO_AUTH")
(>#>) :: RawTransmission -> RawTransmission -> Expectation
noParamsSyntaxTest :: PartyI p => String -> CommandTag p -> Spec
noParamsSyntaxTest description cmd = describe description $ do
it "valid syntax" $ (sampleSig, "abcd", "12345678", cmd) >#> ("", "abcd", "12345678", ERR AUTH)
it "wrong terminator" $ (sampleSig, "bcda", "12345678", (cmd, '=')) >#> ("", "bcda", "12345678", ERR $ CMD UNKNOWN)
it "no signature" $ ("", "cdab", "12345678", cmd) >#> ("", "cdab", "12345678", ERR $ CMD NO_AUTH)
it "no queue ID" $ (sampleSig, "dabc", "", cmd) >#> ("", "dabc", "", ERR $ CMD NO_AUTH)
(>#>) ::
Encoding smp =>
(Maybe C.ASignature, ByteString, ByteString, smp) ->
(Maybe C.ASignature, ByteString, ByteString, BrokerMsg) ->
Expectation
command >#> response = smpServerTest t command `shouldReturn` response
+11 -6
View File
@@ -1,9 +1,11 @@
{-# LANGUAGE TypeApplications #-}
import AgentTests
import ProtocolErrorTests
import AgentTests (agentTests)
import CoreTests.EncodingTests
import CoreTests.ProtocolErrorTests
import CoreTests.VersionRangeTests
import ServerTests
import Simplex.Messaging.Transport (TCP, Transport (..))
import Simplex.Messaging.Transport (TLS, Transport (..))
import Simplex.Messaging.Transport.WebSockets (WS)
import System.Directory (createDirectoryIfMissing, removeDirectoryRecursive)
import Test.Hspec
@@ -12,8 +14,11 @@ main :: IO ()
main = do
createDirectoryIfMissing False "tests/tmp"
hspec $ do
describe "Protocol errors" protocolErrorTests
describe "SMP server via TCP" $ serverTests (transport @TCP)
describe "Core tests" $ do
describe "Encoding tests" encodingTests
describe "Protocol error tests" protocolErrorTests
describe "Version range" versionRangeTests
describe "SMP server via TLS" $ serverTests (transport @TLS)
describe "SMP server via WebSockets" $ serverTests (transport @WS)
describe "SMP client agent" $ agentTests (transport @TCP)
describe "SMP client agent" $ agentTests (transport @TLS)
removeDirectoryRecursive "tests/tmp"
+1
View File
@@ -0,0 +1 @@
server.csr
+28
View File
@@ -0,0 +1,28 @@
To generate fixtures:
(keep these instructions and *openssl_ca.conf* and *openssl_server.conf* files consistent with certificate generation on server)
```sh
# CA certificate (identity/offline)
openssl genpkey -algorithm ED448 -out ca.key
openssl req -new -x509 -days 999999 -config openssl_ca.conf -extensions v3 -key ca.key -out ca.crt
# Server certificate (online)
openssl genpkey -algorithm ED448 -out server.key
openssl req -new -config openssl_server.conf -reqexts v3 -key server.key -out server.csr
openssl x509 -req -days 999999 -extfile openssl_server.conf -extensions v3 -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt
# To pretty-print
openssl x509 -in ca.crt -text -noout
openssl req -in server.csr -text -noout
openssl x509 -in server.crt -text -noout
```
To compute fingerprint for tests:
```haskell
stack ghci
> import Data.X509.Validation (Fingerprint (..))
> Fingerprint fp <- loadFingerprint "tests/fixtures/ca.crt"
> strEncode fp
```
+12
View File
@@ -0,0 +1,12 @@
-----BEGIN CERTIFICATE-----
MIIBtjCCATagAwIBAgIUe2PryrWo0xXX9vcA3WfbCzcdmgAwBQYDK2VxMCoxFjAU
BgNVBAMMDVNNUCBzZXJ2ZXIgQ0ExEDAOBgNVBAoMB1NpbXBsZVgwIBcNMjIwMTEx
MTExNjM5WhgPNDc1OTEyMDgxMTE2MzlaMCoxFjAUBgNVBAMMDVNNUCBzZXJ2ZXIg
Q0ExEDAOBgNVBAoMB1NpbXBsZVgwQzAFBgMrZXEDOgCAcvFwVicR+RLZpiEWPFNR
XYTbf+mFcX1NHIyPQDugFwOCgqJAW1fsjYgFhtQJSMH/lc1N7clfm4CjUzBRMB0G
A1UdDgQWBBQcUJvR7mm26yxMQfCsWgbnwMmJVDAfBgNVHSMEGDAWgBQcUJvR7mm2
6yxMQfCsWgbnwMmJVDAPBgNVHRMBAf8EBTADAQH/MAUGAytlcQNzAAAP/hMPNxyW
fyJi+iJViodU+C/aklnvHtjh5P3AbiVCSUfY6+PEdvkC8Ov0pBAYpYi5ukSNNVXl
ABVRlipB+vOcLQStNyaZ7kXzQ2IO/0btmIidh+G6SP8I4aytYIYYcV5pEUZpG1L1
57g8P29SDv81AA==
-----END CERTIFICATE-----
+4
View File
@@ -0,0 +1,4 @@
-----BEGIN PRIVATE KEY-----
MEcCAQAwBQYDK2VxBDsEOW6vCN7H7or3VsiMaNrFUoCf4LsuhYchWJKataA1mXhN
VIYFo7xWZWBnczDf5hkWQLIHNHm7DDr6+A==
-----END PRIVATE KEY-----
+12
View File
@@ -0,0 +1,12 @@
[req]
distinguished_name = req_distinguished_name
prompt = no
[req_distinguished_name]
CN = SMP server CA
O = SimpleX
[v3]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always
basicConstraints = critical,CA:true
+11
View File
@@ -0,0 +1,11 @@
[req]
distinguished_name = req_distinguished_name
prompt = no
[req_distinguished_name]
CN = localhost
[v3]
basicConstraints = CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyAgreement
extendedKeyUsage = serverAuth
+12
View File
@@ -0,0 +1,12 @@
-----BEGIN CERTIFICATE-----
MIIBvDCCATygAwIBAgIUUgqht3ibEJ9jp5JuroML5Baxa/IwBQYDK2VxMCoxFjAU
BgNVBAMMDVNNUCBzZXJ2ZXIgQ0ExEDAOBgNVBAoMB1NpbXBsZVgwIBcNMjIwMTEx
MTYzOTQ2WhgPNDc1OTEyMDgxNjM5NDZaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDBD
MAUGAytlcQM6AFj+fmjmgOMBFgQ7lXfICSlSaB5sYp1wzhM2IaqgmVhWbDs3Tw7W
oUXFJsWpKku1kFjLH2yhqIInAKNvMG0wCQYDVR0TBAIwADALBgNVHQ8EBAMCA8gw
EwYDVR0lBAwwCgYIKwYBBQUHAwEwHQYDVR0OBBYEFMjbHbxESDI8YDPjJ8kXWjkg
jHxmMB8GA1UdIwQYMBaAFBxQm9HuabbrLExB8KxaBufAyYlUMAUGAytlcQNzAP98
RpFacmjxsc07GwC07uAxUxNgxX4R2nA+0St866uFbdyxApiyn44giG+m+YXCXLjh
1ffKEhhqAPdlpzByLarx8EWgmOsisBCRWrmraksOV15FKxjPqm7OYKp4f9GvieAQ
o/SKOyZVVdcZX7NcuDUxAA==
-----END CERTIFICATE-----
+4
View File
@@ -0,0 +1,4 @@
-----BEGIN PRIVATE KEY-----
MEcCAQAwBQYDK2VxBDsEOZcXSjKQ3FQ9EBfqHmFIidOu4LecpUgi5NVPMhx7mS2d
QNQ0lFasp+EfvLErbs1Sd9xLVxA1hnZJng==
-----END PRIVATE KEY-----