diff --git a/plans/2026-08-09-fast-queue-rotation-plan.md b/plans/2026-08-09-fast-queue-rotation-plan.md new file mode 100644 index 000000000..2af69ba5a --- /dev/null +++ b/plans/2026-08-09-fast-queue-rotation-plan.md @@ -0,0 +1,187 @@ +# Fast queue rotation — implementation plan + +Branch: ep/drop-agent-versions. RFC: ../rfcs/2026-08-09-fast-queue-rotation.md. + +Model: redundant delivery, no flip. `QADD` adds the new receive queue R'. From `QADD` until R' is +secured the sender writes every message to both old and R' (double delivery, not a move); once R' is +secured the sender writes new messages to R' only, while old delivers its already-scheduled tail and +the `QEND` appended to it. `QEND` removes a named queue. The recipient drops duplicates (double +ratchet), so the order and which queue delivers do not matter, as long as every message arrives on at +least one queue. Rotation away from a dead server works because every message up to securing is +scheduled on R' too. + +Roles: A initiates (its receive queue rotates; A receives on R'). B sends to A and secures R'. + +## Why redundant delivery removes the hard parts + +- No boundary, no drain, no last-message id. A never decides how much of old to read. +- A dead old server loses nothing B still holds: every undelivered message is scheduled on R' as well. +- A dead new server does not suspend delivery: old keeps delivering until R' is secured. +- The double ratchet already drops duplicates (`AGENT A_DUPLICATE`) and tolerates bounded reordering, + and the delivery schema already writes one message to several send queues (`enqueueMessageB` + + `enqueueSavedMessageB`). + +## The one ordering constraint + +A must hold R''s secret before it reads any R' data message. A data message reaching R' before the +confirmation is dropped as "no keys" (`processClientMsg`, `(Nothing, Nothing)` arm, line 3611), which +loses it when old is dead. So the confirmation is the first message B sends on R'. R''s delivery +worker does not start while R' is securing, so its accumulated rows cannot outrun the confirmation. +`ICQSndSecure` sends the confirmation and only then starts the worker. This holds on restart too (see +Worker gate). + +## New definitions + +Agent/Protocol.hs +- Condition fast rotation on the existing `rpcAddressSMPAgentVersion` (v8, `Protocol.hs:322`). +- New `AMessage` constructor `QEND SndQAddr` (tag `QE`), the address of the queue to remove. v8-only, + and only sent during fast rotation, so peers below v8 never parse it. +- `SndSwitchStatus` constructors `SSSecuringQueue` (old, while R' secures) and `SSSendingQEND` (old, + after R' is secured — it drains its tail and `QEND` but takes no new messages). +- `InternalCommand` constructor `ICQSndSecure SMP.SenderId`. + +No receive-side switch status, no boundary, no drain state. + +## Schema + +None. `SSSecuringQueue` uses `snd_queues.switch_status`; R' is secured into `rcv_queues.e2e_dh_secret`. +No new columns, no migration. + +## Sender B + +### Dual scheduling from QADD + +`enqueueMessageB` writes a delivery row for the head send queue and for each `filter isActiveSndQ` +tail queue (`Agent.hs:2345`). Adjust the selection two ways: additionally include a securing +replacement queue on a v8 connection (`connAgentVersion cData >= rpcAddressSMPAgentVersion && status == New && isJust dbReplaceQueueId`), +and exclude a terminating queue (`sndSwchStatus == Just SSSendingQEND`). The version guard keeps the +slow path unchanged — there R' is also `New` with a replace reference during `QKEY`/`QUSE`, but it must +not be dual-scheduled. `SSSendingQEND` is a fast-path-only status, so the exclusion never affects the +slow path. The gate below is inert for the slow path anyway, since it never starts R''s worker while +`New`. + +- `QADD` until R' secured: old is the head (active) and R' is the securing replacement, so every `SEND` + writes both rows. old delivers at once; R''s rows accumulate behind its gate. +- R' secured: R' is the head (primary) and old is `SSSendingQEND` (excluded), so a `SEND` writes R' + only. old keeps its worker and delivers whatever was already scheduled on it, plus `QEND`. + +### Worker gate + +`submitPendingMsg` (`Agent.hs:2437`) and `resumeMsgDelivery` (`2421`) — the two `getDeliveryWorker` +callers that start delivery — skip a queue with `status == New && isJust dbReplaceQueueId`, so neither +a `SEND` nor startup starts R''s worker while it secures. Startup resumes delivery through +`resumeMsgDelivery` (`resumeDelivery` line 1848, and `getAllSndQueuesForDelivery` line 1943), so R' is +skipped there; `resumeAllCommands` (1883) resumes R''s `ICQSndSecure`, which secures R' and only then +starts its worker. + +### Steps + +`qAddMsg` (fast branch, under the connection lock, `Agent.hs:3855`): +- Add R' as the slow path does (line 3870): `addConnSndQueue (sq_) {primary = True, dbReplaceQueueId = Just old}`, `New`. +- Duplicate **every** undelivered message on old to R': for each pending row on old + (`SELECT internal_id FROM snd_message_deliveries WHERE conn_id = ? AND snd_queue_id = old AND failed = 0`), + `createSndMsgDelivery db R' internalId`. This is the loss-prevention step: old's not-yet-sent + messages are duplicated onto R', so if old later fails they are already on R'. If old is already + down, nothing was sent and the whole backlog is duplicated. +- `enqueueCommand (Just newSrv) (ICQSndSecure sndId)`; `setSndSwitchStatus SSSecuringQueue` on old + (where the slow path sets `SSSendingQKEY`, line 3874); notify `SWITCH QDSnd SPStarted`. old keeps + delivering; R''s worker is gated. + +`ICQSndSecure sId` (retryable, under `tryWithLock`): +1. If old is already gone, a prior attempt finished; return. Otherwise find R' by `sId`. +2. `secureSndQueue` (SKEY) R' — idempotent, since `sndPrivateKey` was persisted by `qAddMsg` + (`QueueStore/STM.hs:213`: same key → `Right ()`, different key → `AUTH`). +3. Send the confirmation on R' (`sendConfirmation`; empty body, both peers already know each other; + `e2eEncryption_ = Nothing`, no ratchet step). It is the first message on R'. +4. On success, in one transaction: `setSndQueueStatus R' Active` (the gate lifts), `setSndQueuePrimary R'` + (R' becomes the head; its replace reference is cleared), and `setSndSwitchStatus old (Just SSSendingQEND)` + (old takes no new messages but keeps its worker). Then `submitPendingMsg c R'` (the worker starts and + flushes the accumulated rows after the confirmation), `enqueueMessages [old, R'] (QEND oldAddr)` + (appended after old's tail, delivered on both), and notify `SWITCH QDSnd SPSecured`. From here a + `SEND` goes to R' only; old delivers its tail then `QEND` and is removed when `QEND` is sent. + +A temporary error retries from step 1; nothing is torn down. A permanent `AUTH` (should not occur, R' +was secured with B's own key) leaves both queues and surfaces `A_QUEUE`. + +`QEND` sent — new `AM_QEND_` arm in `runSmpQueueMsgDelivery`, modelled on `AM_QTEST_` (`Agent.hs:2567`): +on a successful send of `QEND addr`, remove the named send queue (`TM.delete` its worker, +`deleteConnSndQueue addr`), make the remaining queue the sole primary (`setSndQueuePrimary`, which +clears its `replace_snd_queue_id`), and notify `SWITCH QDSnd SPCompleted` (as `AM_QTEST_` does, line +2591). This re-primary is a no-op on the send side — step 4 already made R' primary before `QEND` was +enqueued. The handler is idempotent — a second `QEND` send finds the named queue already gone and does +nothing. `QEND` is sent on both queues; removing old's send queue also drops any `QEND` still pending +on old. The R' copy reliably removes old +and reaches A even when old is dead; the old copy is best effort. Once old's send queue is gone, `SEND` +schedules to R' only. + +## Recipient A + +A is subscribed to old (primary, `RSSendingQADD`) and R' (created at rotation start, +`dbReplaceQueueId = old`). + +- **Confirmation on R'.** In `processClientMsg`, `(Nothing, Just e2ePubKey)` case, add an arm before the + `senderCanSecure` arm (`Agent.hs:3476`), guarded by `isJust (dbReplaceQueueId rq)`. In one + transaction: `setRcvQueueConfirmedE2E rq (C.dh' e2ePubKey e2ePrivKey) (min v phVer)` (secures R') and + `setRcvQueuePrimary R'` (clears R''s replace reference). Then `ack`, and notify `SWITCH QDRcv SPConfirmed`. + No conn-info processing, no ratchet step, no deferral. Redelivery is idempotent: R' now has `e2e_dh_secret`, so a re-sent + confirmation reaches the `(Just e2eDh, Just _)` arm (line 3608) and is acked — correct here, since + there is no backlog to hold. +- **Data on R'.** With R''s replace reference cleared, a data message on R' takes the ordinary path + (`(_, dbReplaceQueueId=Nothing)`, line 3503) — no old-deletion, no `RSSendingQUSE` check. A copy + already read on old is dropped as `A_DUPLICATE`; a copy read first on R' advances the ratchet and + old's copy is then the duplicate. +- **`QEND oldAddr` on either queue.** New `AMessage` handler (`qEndMsg`, a `qDuplex` handler like + `qAddMsg`): `findRQ oldAddr` the receive queue to remove. Mark it deleted (`setRcvQueueDeleted`, so + `getRcvQueuesByConnId_`'s `deleted = 0` filter excludes it at once and a restart does not resurrect + it) and `enqueueCommand (Just oldServer) (ICDeleteRcvQueue oldRcvId)` for the server `DEL` and record + removal — the async, crash-safe path `abortConnectionSwitch` uses, which resumes on restart and does + not block `QEND`, **not** the synchronous `deleteQueue` of `finalizeSwitch`, which would stall if old + is unreachable. `ICDeleteRcvQueue` (`Agent.hs:2224`) currently retries a temporary error forever; + bound it with the same persisted `rcv_queues.delete_errors`/`deleteErrorCount` mechanism `deleteQueueRec` + uses (2884): on a temporary error `incRcvDeleteErrors`, and at the limit `deleteConnRcvQueue` and + stop. The count is in the database, so the bound survives restarts, and its only other caller + (`abortConnectionSwitch'`, 2739) deletes an alive queue that succeeds well before the limit. `qEndMsg` + does **not** re-primary R' — the confirmation arm (above) owns R''s primary flag and replace + reference. `QEND` on old and the confirmation on R' travel on different queues with no order between + them, so `QEND` on old can be processed first (it sits only behind old's tail); re-primarying then + would clear R''s `dbReplaceQueueId` and the later confirmation would miss the rotation arm and never + secure R'. So `qEndMsg` only removes the named queue. Re-create the notification subscription + (`when enableNtfs $ sendNtfSubCommand ns (NSCCreate, [connId])`); notify + `SWITCH QDRcv SPCompleted`; `ackDel` the `QEND`. Received on both queues, the second finds it already + marked deleted and is a no-op. + +No drain, no boundary, no finalize command. Old is removed when `QEND` arrives, not by counting. + +## Abort / version + +- Fast rotation runs only when `connAgentVersion >= v8`; otherwise the `QKEY`/`QUSE` slow path runs + unchanged. +- `canAbortRcvSwitch` (`Agent/Store.hs:210`) returns false for `RSSendingQADD` when `connAgentVersion >= v8` + (at v8 B always chooses fast, so A treats a sent `QADD` as committed). Its signature gains + `connAgentVersion`; both callers pass it from `cData` — `abortConnectionSwitch'` (2730) and + `rcvQueueInfo` in `connectionStats` (2976). + +## Losses and duplication + +- No boundary loss: the `QADD` step **duplicates** old's entire undelivered backlog onto R', and every + later message up to securing is scheduled on both queues, so if old fails it loses nothing B still + holds. The only messages old can strand are those its server already accepted but had not handed to + A — the ordinary store-and-forward risk, present whenever a server fails with unread messages, and + empty if old was already down (a down server accepted nothing). +- Duplicates: the double ratchet drops them (`A_DUPLICATE`); `checkMsgIntegrity`'s `MsgDuplicate` is + only a flag, not the mechanism. +- The 512 skip bound (`Crypto/Ratchet.hs:953`) does not bite on the rotation: each queue delivers in + order and every message B still holds is on R', so A reads a contiguous stream with only small + cross-queue reordering. A store-and-forward residual (above) is an ordinary loss, not introduced here. + +## Tests + +- new/new, old stopped right after `QADD`: rotation completes; all messages delivered on R'; old + removed by `QEND` on R'. +- new/new, both alive: messages delivered on both, deduped; old removed by `QEND`. +- new/old and old/new: fall back to the `QKEY`/`QUSE` path. +- crash during securing: restart does not start R''s worker; `ICQSndSecure` resumes, secures R', + starts the worker, sends `QEND`. +- `QEND` received on both queues: old removed once, the second receipt is a no-op. +- `QEND` on old processed before the confirmation on R': R' still secures, because `qEndMsg` does not + clear R''s replace reference; rotation completes. diff --git a/protocol/diagrams/duplex-messaging/queue-rotation-fast.mmd b/protocol/diagrams/duplex-messaging/queue-rotation-fast.mmd index 75887dd0b..952729f4a 100644 --- a/protocol/diagrams/duplex-messaging/queue-rotation-fast.mmd +++ b/protocol/diagrams/duplex-messaging/queue-rotation-fast.mmd @@ -5,13 +5,18 @@ sequenceDiagram participant S as Server
that has A's send queue
(B's receive queue) participant B as Bob - A ->> R': NEW: create new queue
(allow SKEY) - A ->> S: SEND: QADD (R'): send address
of the new queue(s) + A ->> R': NEW: create new queue (SKEY allowed) + A ->> S: SEND: QADD (R') S ->> B: MSG: QADD (R') - B ->> R': SKEY: secure new queue - B ->> R': SEND: QTEST - R' ->> A: MSG: QTEST - A ->> R: DEL: delete the old queue - B ->> R': SEND: send messages to the new queue - R' ->> A: MSG: receive messages from the new queue - \ No newline at end of file + B ->> R: SEND: messages (also scheduled on R') + R ->> A: MSG: messages + B ->> R': SKEY: authorize B as sender + B ->> R': SEND: confirmation (establishes R' secret) + R' ->> A: MSG: confirmation (A secures R') + B ->> R': SEND: held copies (deduped), then new messages + R' ->> A: MSG: messages + B ->> R: SEND: remaining tail, then QEND + R ->> A: MSG: QEND + B ->> R': SEND: QEND + R' ->> A: MSG: QEND + A ->> R: DEL: delete the current queue diff --git a/protocol/diagrams/duplex-messaging/queue-rotation-fast.svg b/protocol/diagrams/duplex-messaging/queue-rotation-fast.svg index 823074823..77b986d75 100644 --- a/protocol/diagrams/duplex-messaging/queue-rotation-fast.svg +++ b/protocol/diagrams/duplex-messaging/queue-rotation-fast.svg @@ -1,3 +1,3 @@ -BobServerthat has A's send queue(B's receive queue)New serverthat has the new A'sreceive queueCurrent serverthat has A'sreceive queueAliceBobServerthat has A's send queue(B's receive queue)New serverthat has the new A'sreceive queueCurrent serverthat has A'sreceive queueAliceNEW: create new queue(allow SKEY)SEND: QADD (R'): send addressof the new queue(s)MSG: QADD (R')SKEY: secure new queueSEND: QTESTMSG: QTESTDEL: delete the old queueSEND: send messages to the new queueMSG: receive messages from the new queue \ No newline at end of file +BobServerthat has A's send queue(B's receive queue)New serverthat has the new A'sreceive queueCurrent serverthat has A'sreceive queueAliceBobServerthat has A's send queue(B's receive queue)New serverthat has the new A'sreceive queueCurrent serverthat has A'sreceive queueAliceNEW: create new queue (SKEY allowed)SEND: QADD (R')MSG: QADD (R')SEND: messages (also scheduled on R')MSG: messagesSKEY: authorize B as senderSEND: confirmation (establishes R' secret)MSG: confirmation (A secures R')SEND: held copies (deduped), then new messagesMSG: messagesSEND: remaining tail, then QENDMSG: QENDSEND: QENDMSG: QENDDEL: delete the current queue \ No newline at end of file diff --git a/rfcs/2026-08-09-fast-queue-rotation.md b/rfcs/2026-08-09-fast-queue-rotation.md new file mode 100644 index 000000000..d5cb3cf55 --- /dev/null +++ b/rfcs/2026-08-09-fast-queue-rotation.md @@ -0,0 +1,97 @@ +--- +Proposed: 2026-08-09 +Protocol: agent-protocol v8 +Diagram: ../protocol/diagrams/duplex-messaging/queue-rotation-fast.svg +--- + +# Fast queue rotation + +## Problem + +In the current rotation the peer returns `QKEY` to the initiator over the initiator's current +receiving queue. When that queue's server is unavailable the rotation cannot complete, so a client +cannot move away from a failed server. + +## Solution + +Both the current rotation and v8 add a queue, deliver to both queues while the rotation is in +progress, and remove the old queue; the recipient drops duplicates in both. The main difference is where +the new queue's secret is established. In the current rotation it is established over the current +queue, by `QKEY`, so it cannot complete when the current server is down. In v8 the peer establishes the +new queue's secret over the new queue itself — a confirmation it sends on R' — so establishing the +secret no longer depends on the current queue, and the rotation completes even when the current server +is down. + +v8 also starts writing to both queues earlier: from the moment the queue is added, including the +current queue's not-yet-delivered backlog. So the initiator adds the new queue with `QADD`; from that +point the peer writes every message to both the current queue and R'. Once R' is secured the peer +writes new messages to it alone, while the current queue delivers whatever was already scheduled on it +and a final `QEND`, and is then removed. Because the recipient drops duplicates, neither the order of +arrival nor which queue carries a message matters, provided each message arrives on at least one queue +— with one exception, the confirmation, which is always the first message on R'. A dead new queue does +not stop delivery either, because the current queue keeps delivering until R' is secured. + +Roles: A initiates (its receiving queue rotates; A receives on the new queue R'). B is the peer (B +holds the sending queue to A, secures R', and delivers to both). + +Sequence: + + A -> R' : create new queue (messaging mode, SKEY allowed) + A -> S -> B : QADD(R') (over A's sending queue; A's current server untouched) + B : from QADD, schedule every message and the current backlog on both queues + B -> current : deliver the scheduled messages (R' holds its copies while securing) + B -> R' : SKEY (authorize B as sender) + B -> R' : confirmation (empty; establishes R' secret; first message on R') + B -> R' : deliver R''s held copies (A dedups), then new messages to R' only + B -> current : deliver the remaining tail, then QEND(current) + B -> R' : QEND(current) + A : on QEND, delete the current queue; keep receiving on R' + +## Confirmation + +The confirmation is the only message a recipient can read on a queue that is not yet secured, and it +establishes the queue's shared secret without depending on the current queue. Because a data message +that reached R' before the confirmation could not be read, the confirmation is the first message the +peer sends on R', and the peer does not start ordinary delivery on R' until the confirmation has been +sent. + +The confirmation body is empty: both parties already know each other, so no profile or reply queue is +sent. It is sealed by the queue's box (keyed by the shared secret being established) and is not +additionally encrypted with the double ratchet, so rotation does not advance the message ratchet. + +## Termination + +`QEND` names a queue to remove and is delivered on both queues. On receipt the recipient deletes the +named queue; on send the peer removes its sending queue of that address. `QEND` is a general +queue-removal message — the peer can remove either queue with it — so on the wire a rotation is the +addition of a queue (`QADD`) and the later removal of the replaced one (`QEND`), each an ordinary +operation on the queue set rather than a `QTEST`-style completion. Delivering `QEND` on the removed +queue is best effort; the copy on the surviving queue removes it and reaches the recipient even when +the removed server is dead. + +## Per-queue secret + +R' secret is a fresh Diffie-Hellman between the peer's queue key, sent in the confirmation header, and +the initiator's R' key. It does not depend on any current queue, so redundancy of current queues is +unaffected. + +## Compatibility + +Fast rotation runs only when the connection's agreed agent protocol version is 8 or higher; the peer +chooses it. Otherwise the `QKEY`/`QUSE` exchange is used. `QEND` is defined at version 8 and is only +sent during fast rotation, so peers below version 8 never receive it. + + new A / new B : fast (QADD, confirmation on R', QEND) + new A / old B : slow (old B returns QKEY; new A keeps the QKEY/QUSE handling) + old A / new B : slow (agreed version below 8; new B returns QKEY) + old A / old B : slow + +The recipient does not choose by version; it reacts to whichever message arrives — `QKEY`, or a +confirmation on R' followed later by `QEND`. + +## Dead current server + +The initiator keeps reading messages on the new queue and removes the current queue when `QEND` +arrives there, without waiting for the current server. Nothing is lost, because every message is +scheduled on the new queue; the only cleanup that a dead current server delays is the deletion +of its queue, which is retried a bounded number of times and then abandoned. diff --git a/simplexmq.cabal b/simplexmq.cabal index 0019bc018..db6c0f31d 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -1,7 +1,7 @@ cabal-version: 3.0 name: simplexmq -version: 7.1.0.0 +version: 7.1.0.4 synopsis: SimpleXMQ message broker description: This package includes <./docs/Simplex-Messaging-Server.html server>, <./docs/Simplex-Messaging-Client.html client> and @@ -152,6 +152,7 @@ library Simplex.Messaging.Protocol Simplex.Messaging.Protocol.Types Simplex.Messaging.Server.Expiration + Simplex.Messaging.Server.Information Simplex.Messaging.Server.QueueStore.Postgres.Config Simplex.Messaging.Server.QueueStore.QueueInfo Simplex.Messaging.ServiceScheme @@ -268,7 +269,6 @@ library Simplex.Messaging.Server.CLI Simplex.Messaging.Server.Control Simplex.Messaging.Server.Env.STM - Simplex.Messaging.Server.Information Simplex.Messaging.Server.Main Simplex.Messaging.Server.Main.GitCommit Simplex.Messaging.Server.Main.Init diff --git a/src/Simplex/FileTransfer/Util.hs b/src/Simplex/FileTransfer/Util.hs index fad920747..0714f090c 100644 --- a/src/Simplex/FileTransfer/Util.hs +++ b/src/Simplex/FileTransfer/Util.hs @@ -1,19 +1,26 @@ module Simplex.FileTransfer.Util ( uniqueCombine, + safeFileNameStr, removePath, ) where import Simplex.Messaging.Util (ifM, whenM) -import System.FilePath (splitExtensions, ()) +import System.FilePath (makeValid, splitExtensions, takeFileName, ()) import UnliftIO import UnliftIO.Directory +safeFileNameStr :: String -> String +safeFileNameStr = notDots . makeValid . takeFileName + where + notDots n = if n == "." || n == ".." then "_" else n + +-- | The file name is sanitized, so the combined path cannot escape the folder. uniqueCombine :: MonadIO m => FilePath -> String -> m FilePath uniqueCombine filePath fileName = tryCombine (0 :: Int) where tryCombine n = - let (name, ext) = splitExtensions fileName + let (name, ext) = splitExtensions $ safeFileNameStr fileName suffix = if n == 0 then "" else "_" <> show n f = filePath (name <> suffix <> ext) in ifM (doesPathExist f) (tryCombine $ n + 1) (pure f) diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 56f474a0f..ca910e1d2 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -48,7 +48,7 @@ module Simplex.Messaging.Agent createUser, deleteUser, setUserService, - connRequestPQSupport, + connRequestAgentVersion, prepareConnectionToCreate, createConnectionAsync, setConnShortLinkAsync, @@ -903,18 +903,16 @@ newConnNoQueues c userId enableNtfs cMode pqSupport = do -- TODO [short links] TBC, but probably we will need async join for contact addresses as the contact will be created after user confirming the connection, -- and join should retry, the same as 1-time invitation joins. joinConnAsync :: AgentClient -> ACorrId -> Bool -> ConnId -> Bool -> ConnectionRequestUri c -> ConnInfo -> PQSupport -> SubscriptionMode -> AM () -joinConnAsync c corrId updateConn connId enableNtfs cReqUri@CRInvitationUri {} cInfo pqSup subMode = do +joinConnAsync c corrId updateConn connId enableNtfs cReqUri@CRInvitationUri {} cInfo pqSupport subMode = do when updateConn $ throwE $ CMD PROHIBITED "joinConnAsync: updateConn not allowed for invitation URI" withInvLock c (strEncode cReqUri) "joinConnAsync" $ lift (compatibleInvitationUri cReqUri) >>= \case - Just (_, Compatible (CR.E2ERatchetParams v _ _ _), Compatible connAgentVersion) -> do - let pqSupport = pqSup `CR.pqSupportAnd` versionPQSupport_ connAgentVersion (Just v) + Just _ -> enqueueCommand c corrId connId Nothing $ AClientCommand $ JOIN (JRConnReq enableNtfs (ACR sConnectionMode cReqUri) pqSupport) subMode cInfo Nothing -> throwE $ AGENT A_VERSION -joinConnAsync c corrId updateConn connId enableNtfs cReqUri@CRContactUri {} cInfo pqSup subMode = +joinConnAsync c corrId updateConn connId enableNtfs cReqUri@CRContactUri {} cInfo pqSupport subMode = lift (compatibleContactUri cReqUri) >>= \case - Just (_, rks_, Compatible connAgentVersion) -> do - let pqSupport = pqSup `CR.pqSupportAnd` versionPQSupport_ connAgentVersion (addrKeysE2EVersion <$> rks_) + Just (_, _, Compatible connAgentVersion) -> do when updateConn $ withStore' c $ \db -> updateNewConnJoin db connId connAgentVersion pqSupport enableNtfs enqueueCommand c corrId connId Nothing $ AClientCommand $ JOIN (JRConnReq enableNtfs (ACR sConnectionMode cReqUri) pqSupport) subMode cInfo Nothing -> throwE $ AGENT A_VERSION @@ -1371,21 +1369,20 @@ newQueueNtfSubscription c RcvQueue {userId, connId, server, clientNtfCreds} ntfS liftIO $ sendNtfSubCommand ns (NSCCreate, [connId]) newConnToJoin :: forall c. AgentClient -> UserId -> ConnId -> Bool -> Maybe UTCTime -> ConnectionRequestUri c -> PQSupport -> AM ConnId -newConnToJoin c userId connId enableNtfs serviceRequestExpiresAt cReq pqSup = case cReq of +newConnToJoin c userId connId enableNtfs serviceRequestExpiresAt cReq pqSupport = case cReq of CRInvitationUri {} -> lift (compatibleInvitationUri cReq) >>= \case - Just (_, Compatible (CR.E2ERatchetParams v _ _ _), aVersion) -> create aVersion (Just v) + Just (_, _, aVersion) -> create aVersion Nothing -> throwE $ AGENT A_VERSION CRContactUri {} -> lift (compatibleContactUri cReq) >>= \case - Just (_, rks_, aVersion) -> create aVersion (addrKeysE2EVersion <$> rks_) + Just (_, _, aVersion) -> create aVersion Nothing -> throwE $ AGENT A_VERSION where - create :: Compatible VersionSMPA -> Maybe CR.VersionE2E -> AM ConnId - create (Compatible connAgentVersion) e2eV_ = do + create :: Compatible VersionSMPA -> AM ConnId + create (Compatible connAgentVersion) = do g <- asks random - let pqSupport = pqSup `CR.pqSupportAnd` versionPQSupport_ connAgentVersion e2eV_ - cData = ConnData {userId, connId, connAgentVersion, enableNtfs, lastExternalSndId = 0, deleted = False, ratchetSyncState = RSOk, pqSupport, serviceRequestExpiresAt} + let cData = ConnData {userId, connId, connAgentVersion, enableNtfs, lastExternalSndId = 0, deleted = False, ratchetSyncState = RSOk, pqSupport, serviceRequestExpiresAt} withStore c $ \db -> createNewConn db g cData SCMInvitation newConnToAccept :: AgentClient -> UserId -> ConnId -> Bool -> InvitationId -> PQSupport -> AM ConnId @@ -1411,12 +1408,11 @@ joinConn c nm userId connId enableNtfs cReq cInfo pqSupport subMode = do joinConnSrv c nm userId connId enableNtfs cReq cInfo pqSupport subMode srv startJoinInvitation :: AgentClient -> UserId -> ConnId -> Maybe SndQueue -> Bool -> ConnectionRequestUri 'CMInvitation -> PQSupport -> AM ((ConnData, SndQueue), (Maybe (CR.SndE2ERatchetParams 'C.X448), Maybe SMP.LinkId)) -startJoinInvitation c userId connId sq_ enableNtfs cReqUri pqSup = +startJoinInvitation c userId connId sq_ enableNtfs cReqUri pqSupport = lift (compatibleInvitationUri cReqUri) >>= \case Just (qInfo, Compatible e2eRcvParams@(CR.E2ERatchetParams v _ _ _), Compatible connAgentVersion) -> do -- this case avoids re-generating queue keys and subsequent failure of SKEY that timed out -- e2ePubKey is always present, it's Maybe historically - let pqSupport = pqSup `CR.pqSupportAnd` versionPQSupport_ connAgentVersion (Just v) g <- asks random maxSupported <- asks $ maxVersion . e2eEncryptVRange . config let cData = ConnData {userId, connId, connAgentVersion, enableNtfs, lastExternalSndId = 0, deleted = False, ratchetSyncState = RSOk, pqSupport, serviceRequestExpiresAt = Nothing} @@ -1445,7 +1441,7 @@ startJoinInvitation c userId connId sq_ enableNtfs cReqUri pqSup = createRatchet_ :: DB.Connection -> TVar ChaChaDRG -> ConnId -> CR.VersionE2E -> PQSupport -> CR.RcvE2ERatchetParams 'C.X448 -> ExceptT StoreError IO (CR.RatchetX448, CR.SndE2ERatchetParams 'C.X448) createRatchet_ db g connId maxSupported pqSupport e2eRcvParams@(CR.E2ERatchetParams v _ rcDHRr kem_) = do - (pks, e2eSndParams) <- liftIO $ CR.generateSndE2EParams g v (CR.replyKEM_ v kem_ pqSupport) + (pks, e2eSndParams) <- liftIO $ CR.generateSndE2EParams g v (CR.replyKEM_ kem_ pqSupport) (_, rcDHRs) <- atomically $ C.generateKeyPair g rcParams <- liftEitherWith (SEAgentError . cryptoError) $ CR.pqX3dhSnd pks e2eRcvParams let rcVs = CR.RatchetVersions {current = v, maxSupported} @@ -1463,14 +1459,13 @@ startJoinInvitationDR c userId ConnData {connId} DRInvitation {ratchetState, rep liftIO $ createRatchet db connId ratchetState ExceptT $ updateNewConnSnd db connId q -connRequestPQSupport :: AgentClient -> PQSupport -> ConnectionRequestUri c -> IO (Maybe (VersionSMPA, PQSupport)) -connRequestPQSupport c pqSup cReq = withAgentEnv' c $ case cReq of - CRInvitationUri {} -> invPQSupported <$$> compatibleInvitationUri cReq - where - invPQSupported (_, Compatible (CR.E2ERatchetParams e2eV _ _ _), Compatible agentV) = (agentV, pqSup `CR.pqSupportAnd` versionPQSupport_ agentV (Just e2eV)) - CRContactUri {} -> ctPQSupported <$$> compatibleContactUri cReq - where - ctPQSupported (_, rks_, Compatible agentV) = (agentV, pqSup `CR.pqSupportAnd` versionPQSupport_ agentV (addrKeysE2EVersion <$> rks_)) +connRequestAgentVersion :: AgentClient -> ConnectionRequestUri c -> IO (Maybe VersionSMPA) +connRequestAgentVersion c cReq = withAgentEnv' c $ case cReq of + CRInvitationUri {} -> aVersion <$$> compatibleInvitationUri cReq + CRContactUri {} -> aVersion <$$> compatibleContactUri cReq + where + aVersion :: (Compatible SMPQueueInfo, r, Compatible VersionSMPA) -> VersionSMPA + aVersion (_, _, Compatible agentV) = agentV compatibleInvitationUri :: ConnectionRequestUri 'CMInvitation -> AM' (Maybe (Compatible SMPQueueInfo, Compatible (CR.RcvE2ERatchetParams 'C.X448), Compatible VersionSMPA)) compatibleInvitationUri (CRInvitationUri ConnReqUriData {crAgentVRange, crSmpQueues = (qUri :| _)} e2eRcvParamsUri) = do @@ -1495,13 +1490,6 @@ compatibleContactUri (CRContactUri ConnReqUriData {crAgentVRange, crSmpQueues = Just (ratchetKeyId, e2eRcvParams) -> Just . (ratchetKeyId,) <$> (e2eRcvParams `compatibleVersion` e2eVR) -versionPQSupport_ :: VersionSMPA -> Maybe CR.VersionE2E -> PQSupport -versionPQSupport_ agentV e2eV_ = PQSupport $ agentV >= pqdrSMPAgentVersion && maybe True (>= CR.pqRatchetE2EEncryptVersion) e2eV_ -{-# INLINE versionPQSupport_ #-} - -addrKeysE2EVersion :: (RatchetKeyId, Compatible (CR.RcvE2ERatchetParams 'C.X448)) -> CR.VersionE2E -addrKeysE2EVersion (_, Compatible (CR.E2ERatchetParams e2eV _ _ _)) = e2eV - joinConnSrv :: AgentClient -> NetworkRequestMode -> UserId -> ConnId -> Bool -> ConnectionRequestUri c -> ConnInfo -> PQSupport -> SubscriptionMode -> SMPServerWithAuth -> AM SndQueueSecured joinConnSrv c nm userId connId enableNtfs cReq cInfo pqSup subMode srv = joinConnSrv' c nm userId connId enableNtfs cReq cInfo pqSup subMode srv $ \replyQInfo _ -> AgentConnInfoReply (replyQInfo :| []) cInfo @@ -1522,14 +1510,14 @@ joinConnSrv' c nm userId connId enableNtfs inv@CRInvitationUri {} cInfo pqSup su ((cData, sq), (e2eSndParams, lnkId_)) <- startJoinInvitation c userId connId sq_ enableNtfs inv pqSup secureConfirmQueue c nm cData rq_ sq srv cInfo e2eSndParams subMode >>= (mapM_ (delInvSL c connId srv) lnkId_ $>) -joinConnSrv' c nm userId connId enableNtfs cReqUri@CRContactUri {} cInfo pqSup subMode srv mkInner = +joinConnSrv' c nm userId connId enableNtfs cReqUri@CRContactUri {} cInfo pqSupport subMode srv mkInner = lift (compatibleContactUri cReqUri) >>= \case Just (qInfo, ratchet_, Compatible v) -> withInvLock c (strEncode cReqUri) "joinConnSrv" $ do SomeConn cType conn <- withStore c (`getConn` connId) envelope <- case ratchet_ of Nothing -> do - let pqInitKeys = CR.joinContactInitialKeys (v >= pqdrSMPAgentVersion) pqSup + let pqInitKeys = CR.joinContactInitialKeys pqSupport CCLink cReq _ <- case conn of NewConnection _ -> newRcvConnSrv c NRMBackground userId connId enableNtfs SCMInvitation Nothing Nothing pqInitKeys False subMode srv RcvConnection _ rq -> mkJoinInvitation rq pqInitKeys @@ -1538,8 +1526,7 @@ joinConnSrv' c nm userId connId enableNtfs cReqUri@CRContactUri {} cInfo pqSup s Just (ratchetKeyId, Compatible e2eParams@(CR.E2ERatchetParams e2eV _ _ _)) -> do g <- asks random e2eVR <- asks $ e2eEncryptVRange . config - let pqSupport = pqSup `CR.pqSupportAnd` versionPQSupport_ v (Just e2eV) - maxV = maxVersion e2eVR + let maxV = maxVersion e2eVR rq <- case conn of NewConnection _ -> do e2eKeys <- atomically $ C.generateKeyPair g @@ -2237,8 +2224,12 @@ runCommandProcessing c@AgentClient {subQ} connId server_ Worker {doWork} = do ICDeleteConn -> withStore' c (`deleteCommand` cmdId) ICDeleteRcvQueue rId -> withServer $ \srv -> tryWithLock "ICDeleteRcvQueue" $ do rq <- withStore c (\db -> getDeletedRcvQueue db connId srv rId) - deleteQueue c NRMBackground rq - withStore' c (`deleteConnRcvQueue` rq) + maxErrs <- asks $ deleteErrorCount . config + tryAllErrors (deleteQueue c NRMBackground rq) >>= \case + Left e | temporaryOrHostError e && deleteErrors rq + 1 < maxErrs -> do + withStore' c (`incRcvDeleteErrors` rq) + throwE e + _ -> withStore' c (`deleteConnRcvQueue` rq) ICQSecure rId senderKey -> withServer $ \srv -> tryWithLock "ICQSecure" . withDuplexConn $ \(DuplexConnection cData rqs sqs) -> case find (sameQueue (srv, rId)) rqs of @@ -2258,6 +2249,31 @@ runCommandProcessing c@AgentClient {subQ} connId server_ Worker {doWork} = do notify $ SWITCH QDRcv SPSecured cStats _ -> internalErr "ICQSecure: no switching queue found" _ -> internalErr "ICQSecure: queue address not found in connection" + ICQSndSecure sId -> + withServer $ \srv -> tryWithLock "ICQSndSecure" . withDuplexConn $ \(DuplexConnection cData@ConnData {connAgentVersion} rqs sqs) -> + case findQ (srv, sId) sqs of + Nothing -> internalErr "ICQSndSecure: queue address not found in connection" + Just sq'@SndQueue {dbReplaceQueueId} -> + case dbReplaceQueueId >>= \replaceQId -> find ((replaceQId ==) . dbQId) sqs of + Just oldSq -> do + secureSndQueue c NRMBackground sq' + let confMsg = smpEncode $ AgentConfirmation {agentVersion = connAgentVersion, e2eEncryption_ = Nothing, encConnInfo = ""} + void $ sendConfirmation c NRMBackground sq' confMsg + oldSq' <- withStore' c $ \db -> do + setSndQueueStatus db sq' Active + setSndQueuePrimary db connId sq' + setSndSwitchStatus db oldSq $ Just SSSendingQEND + let sq'' = (sq' :: SndQueue) {status = Active, primary = True, dbReplaceQueueId = Nothing} + pending <- withStore' c $ \db -> countSndQueueDeliveries db sq'' + atomically $ modifyTVar' (msgDeliveryOp c) $ \s -> s {opsInProgress = opsInProgress s + pending} + lift $ resumeMsgDelivery c sq'' + void $ enqueueMessages c cData [oldSq, sq''] SMP.noMsgFlags $ QEND [qAddress oldSq] + let conn' = DuplexConnection cData rqs (updatedQs oldSq' $ updatedQs sq'' sqs) + cStats <- connectionStats c conn' + notify $ SWITCH QDSnd SPSecured cStats + Nothing -> + forM_ (find (\q -> sndSwchStatus q == Just SSSendingQEND) sqs) $ \oldSq -> + void $ enqueueMessages c cData [oldSq, sq'] SMP.noMsgFlags $ QEND [qAddress oldSq] ICQDelete rId -> do withServer $ \srv -> tryWithLock "ICQDelete" . withDuplexConn $ \(DuplexConnection cData@ConnData {enableNtfs} rqs sqs) -> do case removeQ (srv, rId) rqs of @@ -2282,9 +2298,11 @@ runCommandProcessing c@AgentClient {subQ} connId server_ Worker {doWork} = do notify $ SWITCH QDRcv SPCompleted cStats _ -> internalErr "ICQDelete: cannot delete the only queue in connection" where - ack srv rId srvMsgId = do - rq <- withStore c $ \db -> getRcvQueue db connId srv rId - ackQueueMessage c rq srvMsgId + ack srv rId srvMsgId = + withStore' c (\db -> getRcvQueue db connId srv rId) >>= \case + Right rq -> ackQueueMessage c rq srvMsgId + Left SEConnNotFound -> pure Nothing + Left e -> throwE $ storeError e secure :: RcvQueue -> SMP.SndPublicAuthKey -> AM () secure rq@RcvQueue {server} senderKey = do secureQueue c NRMBackground rq senderKey @@ -2339,8 +2357,10 @@ enqueueMessagesB c reqs = do enqueueSavedMessageB c $ mapMaybe snd $ rights $ toList reqs' pure $ fst <$$> reqs' -isActiveSndQ :: SndQueue -> Bool -isActiveSndQ SndQueue {status} = status == Secured || status == Active +isActiveSndQ :: ConnData -> SndQueue -> Bool +isActiveSndQ ConnData {connAgentVersion} sq@SndQueue {status, sndSwchStatus} = + sndSwchStatus /= Just SSSendingQEND + && (status == Secured || status == Active || (connAgentVersion >= rpcAddressSMPAgentVersion && securingSndQueue sq)) {-# INLINE isActiveSndQ #-} enqueueMessage :: AgentClient -> ConnData -> SndQueue -> MsgFlags -> AMessage -> AM (AgentMsgId, PQEncryption) @@ -2354,9 +2374,9 @@ enqueueMessageB c reqs = do cfg <- asks config (_, reqMids) <- unsafeWithStore c $ \db -> do mapAccumLM (\ids r -> storeSentMsg db cfg ids r `E.catchAny` \e -> (ids,) <$> handleInternal e) IM.empty reqs - forME reqMids $ \((csqs_, _, _, _), InternalId msgId, pqSecr) -> forM csqs_ $ \(_, sq :| sqs) -> do + forME reqMids $ \((csqs_, _, _, _), InternalId msgId, pqSecr) -> forM csqs_ $ \(cData, sq :| sqs) -> do submitPendingMsg c sq - let sqs' = filter isActiveSndQ sqs + let sqs' = filter (isActiveSndQ cData) sqs pure ((msgId, pqSecr), if null sqs' then Nothing else Just (sqs', msgId)) where storeSentMsg :: @@ -2436,9 +2456,13 @@ resumeMsgDelivery :: AgentClient -> SndQueue -> AM' () -- hasWork is passed as False to avoid unnecessary write to TMVar: -- - new worker is always created by "some work to do". -- - if the worker already exists, there is no need to "push" it again. -resumeMsgDelivery = void .: getDeliveryWorker False +resumeMsgDelivery c sq = unless (securingSndQueue sq) $ void $ getDeliveryWorker False c sq {-# INLINE resumeMsgDelivery #-} +securingSndQueue :: SndQueue -> Bool +securingSndQueue SndQueue {status, dbReplaceQueueId} = status == New && isJust dbReplaceQueueId +{-# INLINE securingSndQueue #-} + getDeliveryWorker :: Bool -> AgentClient -> SndQueue -> AM' (Worker, TMVar ()) getDeliveryWorker hasWork c sq = getAgentWorker' fst mkLock "msg_delivery" hasWork c (qAddress sq) (smpDeliveryWorkers c) (runSmpQueueMsgDelivery c sq) @@ -2448,7 +2472,7 @@ getDeliveryWorker hasWork c sq = pure (w, retryLock) submitPendingMsg :: AgentClient -> SndQueue -> AM' () -submitPendingMsg c sq = do +submitPendingMsg c sq = unless (securingSndQueue sq) $ do atomically $ modifyTVar' (msgDeliveryOp c) $ \s -> s {opsInProgress = opsInProgress s + 1} void $ getDeliveryWorker True c sq @@ -2519,6 +2543,7 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} sq@SndQueue {userId, connId, server, AM_QKEY_ -> qError msgId "QKEY: AUTH" AM_QUSE_ -> qError msgId "QUSE: AUTH" AM_QTEST_ -> qError msgId "QTEST: AUTH" + AM_QEND_ -> delMsg msgId AM_EREADY_ -> notifyDel msgId err AM_SRV_REQ -> logError "AM_SRV_REQ: unexpected stored message" >> delMsg msgId AM_SRV_RESP -> notifyDel msgId err @@ -2606,6 +2631,20 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} sq@SndQueue {userId, connId, server, _ -> internalErr msgId "sent QTEST: there is only one queue in connection" _ -> internalErr msgId "sent QTEST: queue not in connection or not replacing another queue" _ -> internalErr msgId "QTEST sent not in duplex connection" + AM_QEND_ -> withConnLockNotify c connId "runSmpQueueMsgDelivery AM_QEND_" $ do + SomeConn _ conn <- withStore c (`getConn` connId) + case conn of + DuplexConnection cData' rqs sqs -> + forM (removeQP (\sq' -> sndSwchStatus sq' == Just SSSendingQEND) sqs) $ \case + (oldSq, sq'' : sqs') -> do + atomically $ TM.delete (qAddress oldSq) $ smpDeliveryWorkers c + withStore' c $ \db -> do + deletePendingMsgs db connId oldSq + deleteConnSndQueue db connId oldSq + cStats <- connectionStats c $ DuplexConnection cData' rqs (sq'' :| sqs') + pure ("", connId, AEvt SAEConn $ SWITCH QDSnd SPCompleted cStats) + (_, []) -> pure ("", connId, AEvt SAEConn $ ERR $ INTERNAL "sent QEND: no remaining queue in connection") + _ -> internalErr msgId "QEND sent not in duplex connection" AM_EREADY_ -> pure () AM_SRV_REQ -> logError "AM_SRV_REQ: unexpected stored message" AM_SRV_RESP -> notify $ SSENT mId proxySrv_ @@ -2681,15 +2720,14 @@ ackMessage' c connId msgId rcptInfo_ = withConnLockNotify c connId "ackMessage" del :: AM () del = withStore' c $ \db -> deleteMsg db connId $ InternalId msgId sendRcpt :: Connection 'CDuplex -> AM () - sendRcpt (DuplexConnection cData@ConnData {connAgentVersion} _ sqs) = do + sendRcpt (DuplexConnection cData _ sqs) = do msg@RcvMsg {msgType, msgReceipt} <- withStore c $ \db -> getRcvMsg db connId $ InternalId msgId case rcptInfo_ of Just rcptInfo -> do unless (msgType == AM_A_MSG_) . throwE $ CMD PROHIBITED "ackMessage: receipt not allowed" - when (connAgentVersion >= deliveryRcptsSMPAgentVersion) $ do - let RcvMsg {msgMeta = MsgMeta {sndMsgId}, internalHash} = msg - rcpt = A_RCVD [AMessageReceipt {agentMsgId = sndMsgId, msgHash = internalHash, rcptInfo}] - void $ enqueueMessages c cData sqs SMP.MsgFlags {notification = False} rcpt + let RcvMsg {msgMeta = MsgMeta {sndMsgId}, internalHash} = msg + rcpt = A_RCVD [AMessageReceipt {agentMsgId = sndMsgId, msgHash = internalHash, rcptInfo}] + void $ enqueueMessages c cData sqs SMP.MsgFlags {notification = False} rcpt Nothing -> case (msgType, msgReceipt) of -- only remove sent message if receipt hash was Ok, both to debug and for future redundancy (AM_A_RCVD_, Just MsgReceipt {agentMsgId = sndMsgId, msgRcptStatus = MROk}) -> @@ -2742,7 +2780,7 @@ abortConnectionSwitch' c connId = withStore c (`getConn` connId) >>= \case SomeConn _ (DuplexConnection cData rqs sqs) -> case switchingRQ rqs of Just rq - | canAbortRcvSwitch rq -> do + | canAbortRcvSwitch cData rq -> do when (ratchetSyncSendProhibited cData) $ throwE $ CMD PROHIBITED "abortConnectionSwitch: send prohibited" -- multiple queues to which the connections switches were possible when repeating switch was allowed let (delRqs, keepRqs) = L.partition ((Just (dbQId rq) ==) . dbReplaceQId) rqs @@ -2771,7 +2809,7 @@ synchronizeRatchet' c connId pqSupport' force = withConnLock c connId "synchroni AgentConfig {e2eEncryptVRange} <- asks config g <- asks random (pks, e2eParams) <- liftIO $ CR.generateRcvE2EParams g (maxVersion e2eEncryptVRange) pqSupport' - enqueueRatchetKeyMsgs c sqs e2eParams + enqueueRatchetKeyMsgs c cData' sqs e2eParams withStore' c $ \db -> do setConnRatchetSync db connId RSStarted setRatchetX3dhKeys db connId pks @@ -2957,12 +2995,12 @@ getConnectionRatchetAdHash' c connId = do connectionStats :: AgentClient -> Connection c -> AM ConnectionStats connectionStats c = \case RcvConnection cData rq -> do - rcvQueuesInfo <- (: []) <$> rcvQueueInfo rq + rcvQueuesInfo <- (: []) <$> rcvQueueInfo cData rq pure (stats cData) {rcvQueuesInfo, subStatus = connSubStatus rcvQueuesInfo} SndConnection cData sq -> do pure (stats cData) {sndQueuesInfo = [sndQueueInfo sq]} DuplexConnection cData rqs sqs -> do - rcvQueuesInfo <- mapM rcvQueueInfo (L.toList rqs) + rcvQueuesInfo <- mapM (rcvQueueInfo cData) (L.toList rqs) pure (stats cData) { rcvQueuesInfo, @@ -2970,7 +3008,7 @@ connectionStats c = \case subStatus = connSubStatus rcvQueuesInfo } ContactConnection cData rq -> do - rcvQueuesInfo <- (: []) <$> rcvQueueInfo rq + rcvQueuesInfo <- (: []) <$> rcvQueueInfo cData rq pure (stats cData) {rcvQueuesInfo, subStatus = connSubStatus rcvQueuesInfo} NewConnection cData -> pure $ stats cData @@ -2982,13 +3020,13 @@ connectionStats c = \case rcvQueuesInfo = [], sndQueuesInfo = [], ratchetSyncState, - ratchetSyncSupported = connAgentVersion >= ratchetSyncSMPAgentVersion, + ratchetSyncSupported = True, subStatus = Nothing } - rcvQueueInfo :: RcvQueue -> AM RcvQueueInfo - rcvQueueInfo rq@RcvQueue {server, status, rcvSwchStatus} = do + rcvQueueInfo :: ConnData -> RcvQueue -> AM RcvQueueInfo + rcvQueueInfo cData rq@RcvQueue {server, status, rcvSwchStatus} = do subStatus <- atomically checkQueueSubStatus - pure $ RcvQueueInfo {rcvServer = server, status, rcvSwitchStatus = rcvSwchStatus, canAbortSwitch = canAbortRcvSwitch rq, subStatus} + pure $ RcvQueueInfo {rcvServer = server, status, rcvSwitchStatus = rcvSwchStatus, canAbortSwitch = canAbortRcvSwitch cData rq, subStatus} where checkQueueSubStatus :: STM SubscriptionStatus checkQueueSubStatus = @@ -3479,7 +3517,7 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(userId, srv, _), THandlePar _ -> pure () processClientMsg srvTs msgFlags msgBody = do clientMsg@SMP.ClientMsgEnvelope {cmHeader = SMP.PubHeader phVer e2ePubKey_} <- - parseMessage msgBody + parseMessage "4" msgBody clientVRange <- asks $ smpClientVRange . config unless (phVer `isCompatible` clientVRange || phVer <= agreedClientVerion) . throwE $ AGENT A_VERSION case (e2eDhSecret, e2ePubKey_) of @@ -3536,6 +3574,7 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(userId, srv, _), THandlePar -- no action needed for QTEST -- any message in the new queue will mark it active and trigger deletion of the old queue QTEST _ -> logServer "<--" c srv rId ("MSG :" <> logSecret' srvMsgId) >> ackDel msgId + QEND addrs -> qDuplexAckDel conn'' "QEND" $ qEndMsg srvMsgId addrs EREADY _ -> qDuplexAckDel conn'' "EREADY" $ ereadyMsg rcPrev where qDuplexAckDel :: Connection c -> String -> (Connection 'CDuplex -> AM ()) -> AM ACKd @@ -3567,7 +3606,7 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(userId, srv, _), THandlePar notify $ ERR (AGENT $ A_DUPLICATE $ Just DroppedMsg {brokerTs, attempts}) ackDel internalId else - liftEither (parse smpP (AGENT A_MESSAGE) agentMsgBody) >>= \case + liftEither (parse smpP (AGENT $ A_MESSAGE "parse msg body 1") agentMsgBody) >>= \case AgentMessage _ (A_MSG body) -> do logServer "<--" c srv rId $ "MSG :" <> logSecret' srvMsgId notify $ MSG msgMeta msgFlags body @@ -3602,7 +3641,7 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(userId, srv, _), THandlePar liftIO $ lockConnForUpdate db connId rc <- ExceptT $ getRatchetForUpdate db connId -- ratchet state pre-decryption - required for processing EREADY (agentMsgBody, pqEncryption) <- agentRatchetDecrypt' g db connId rc encAgentMessage - liftEither (parse smpP (SEAgentError $ AGENT A_MESSAGE) agentMsgBody) >>= \case + liftEither (parse smpP (SEAgentError $ AGENT $ A_MESSAGE "parse msg body 2") agentMsgBody) >>= \case agentMsg@(AgentMessage APrivHeader {sndMsgId, prevMsgHash} aMessage) -> do let msgType = agentMessageType agentMsg internalHash = C.sha256Hash agentMsgBody @@ -3688,8 +3727,8 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(userId, srv, _), THandlePar decryptClientMessage :: C.DhSecretX25519 -> SMP.ClientMsgEnvelope -> AM (SMP.PrivHeader, AgentMsgEnvelope) decryptClientMessage e2eDh SMP.ClientMsgEnvelope {cmNonce, cmEncBody} = do clientMsg <- liftEither $ agentCbDecrypt e2eDh cmNonce cmEncBody - SMP.ClientMessage privHeader clientBody <- parseMessage clientMsg - agentEnvelope <- parseMessage clientBody + SMP.ClientMessage privHeader clientBody <- parseMessage "5" clientMsg + agentEnvelope <- parseMessage "6" clientBody -- Version check is removed here, because when connecting via v1 contact address the agent still sends v2 message, -- to allow duplexHandshake mode, in case the receiving agent was updated to v2 after the address was created. -- aVRange <- asks $ smpAgentVRange . config @@ -3698,8 +3737,8 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(userId, srv, _), THandlePar -- else throwE $ AGENT A_VERSION pure (privHeader, agentEnvelope) - parseMessage :: Encoding a => ByteString -> AM a - parseMessage = liftEither . parse smpP (AGENT A_MESSAGE) + parseMessage :: Encoding a => String -> ByteString -> AM a + parseMessage cxt = liftEither . parse smpP (AGENT $ A_MESSAGE $ "parse message " <> cxt) -- checking agreed versions to continue connection in case of client/agent version downgrades checkConfVersions :: VersionSMPA -> VersionSMPC -> AM () @@ -3716,6 +3755,18 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(userId, srv, _), THandlePar checkConfVersions agentVersion phVer let ConnData {pqSupport, serviceRequestExpiresAt} = toConnData conn' case status of + New | isJust (dbReplaceQId rq) -> case conn' of + DuplexConnection cData' rqs sqs -> do + let dhSecret = C.dh' e2ePubKey e2ePrivKey + clientVersion = min agreedClientVerion phVer + withStore' c $ \db -> do + setRcvQueueConfirmedE2E db rq dhSecret clientVersion + setRcvQueuePrimary db connId rq + let rq' = (rq :: RcvQueue) {status = Confirmed, e2eDhSecret = Just dhSecret, smpClientVersion = clientVersion, primary = True, dbReplaceQueueId = Nothing} + conn'' = DuplexConnection cData' (updatedQs rq' rqs) sqs + cStats <- connectionStats c conn'' + notify $ SWITCH QDRcv SPConfirmed cStats + _ -> prohibited "conf: rotation not in duplex connection" New -> case conn' of -- party initiating connection RcvConnection {} -> do @@ -3723,7 +3774,7 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(userId, srv, _), THandlePar -- create ratchet from sent invitation and received confirmation keys Just e2eSndParams -> do keys <- withStore c (`getRatchetX3dhKeys` connId) - processConnInfo =<< initRcvRatchet_ agentVersion pqSupport keys e2eSndParams + processConnInfo =<< initRcvRatchet_ pqSupport keys e2eSndParams -- use ratchet initialized from contact address ratchet keys during invitation Nothing -> withStore' c (`getRatchet` connId) >>= \case Left _ -> prohibited "conf: incorrect state" @@ -3732,7 +3783,7 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(userId, srv, _), THandlePar processConnInfo (rc, pqSupport') = do (agentMsgBody_, rc') <- decryptConnInfo rc encConnInfo case agentMsgBody_ of - Right agentMsgBody -> parseMessage agentMsgBody >>= \case + Right agentMsgBody -> parseMessage "1" agentMsgBody >>= \case AgentConnInfoReply smpQueues connInfo | isNothing serviceRequestExpiresAt -> do processConf rc' connInfo SMPConfirmation {senderKey, e2ePubKey, connInfo, smpReplyQueues = L.toList smpQueues, smpClientVersion = phVer} withStore' c $ \db -> updateRcvMsgHash db connId 1 (InternalRcvId 0) (C.sha256Hash agentMsgBody) @@ -3773,7 +3824,7 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(userId, srv, _), THandlePar DuplexConnection _ (rq'@RcvQueue {smpClientVersion = v'} :| _) _ | isNothing e2eEncryption -> do g <- asks random (agentMsgBody, pqEncryption) <- withStore c $ \db -> runExceptT $ agentRatchetDecrypt g db connId encConnInfo - parseMessage agentMsgBody >>= \case + parseMessage "2" agentMsgBody >>= \case AgentConnInfo connInfo -> do notify $ INFO pqSupport connInfo let dhSecret = C.dh' e2ePubKey e2ePrivKey @@ -3789,15 +3840,14 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(userId, srv, _), THandlePar _ -> prohibited "conf: incorrect state" _ -> prohibited "conf: status /= new" - initRcvRatchet_ :: VersionSMPA -> PQSupport -> CR.RcvE2EPrivRatchetParams 'C.X448 -> CR.SndE2ERatchetParams 'C.X448 -> AM (CR.RatchetX448, PQSupport) - initRcvRatchet_ agentVersion pqSupport pks@(_, pk2, _) (CR.AE2ERatchetParams _ e2eSndParams@(CR.E2ERatchetParams e2eVersion _ _ _)) = do + initRcvRatchet_ :: PQSupport -> CR.RcvE2EPrivRatchetParams 'C.X448 -> CR.SndE2ERatchetParams 'C.X448 -> AM (CR.RatchetX448, PQSupport) + initRcvRatchet_ pqSupport pks@(_, pk2, _) (CR.AE2ERatchetParams _ e2eSndParams@(CR.E2ERatchetParams e2eVersion _ _ _)) = do e2eEncryptVRange <- asks $ e2eEncryptVRange . config unless (e2eVersion `isCompatible` e2eEncryptVRange) $ throwE $ AGENT A_VERSION rcParams <- liftError cryptoError $ CR.pqX3dhRcv pks e2eSndParams let rcVs = CR.RatchetVersions {current = e2eVersion, maxSupported = maxVersion e2eEncryptVRange} - connPQSupport = pqSupport `CR.pqSupportAnd` versionPQSupport_ agentVersion (Just e2eVersion) - rc = CR.initRcvRatchet rcVs pk2 rcParams connPQSupport - pure (rc, connPQSupport) + rc = CR.initRcvRatchet rcVs pk2 rcParams pqSupport + pure (rc, pqSupport) decryptConnInfo :: CR.RatchetX448 -> ByteString -> AM (Either C.CryptoError ByteString, CR.RatchetX448) decryptConnInfo rc encConnInfo = do @@ -3868,7 +3918,7 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(userId, srv, _), THandlePar -- processed by queue sender qAddMsg :: SMP.MsgId -> NonEmpty (SMPQueueUri, Maybe SndQAddr) -> Connection 'CDuplex -> AM () qAddMsg _ ((_, Nothing) :| _) _ = qError "adding queue without switching is not supported" - qAddMsg srvMsgId ((qUri, Just addr) :| _) (DuplexConnection cData' rqs sqs) = do + qAddMsg srvMsgId ((qUri, Just addr) :| _) (DuplexConnection cData'@ConnData {connAgentVersion} rqs sqs) = do when (ratchetSyncSendProhibited cData') $ throwE $ AGENT (A_QUEUE "ratchet is not synchronized") clientVRange <- asks $ smpClientVRange . config case qUri `compatibleVersion` clientVRange of @@ -3885,9 +3935,17 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(userId, srv, _), THandlePar liftIO $ mapM_ (deleteConnSndQueue db connId) delSqs addConnSndQueue db connId (sq_ :: NewSndQueue) {primary = True, dbReplaceQueueId = Just dbQueueId} logServer "<--" c srv rId $ "MSG :" <> logSecret' srvMsgId <> " " <> logSecret (senderId queueAddress) - let sqInfo' = (sqInfo :: SMPQueueInfo) {queueAddress = queueAddress {dhPublicKey}} - void . enqueueMessages c cData' sqs SMP.noMsgFlags $ QKEY [(sqInfo', C.toPublic sndPrivateKey)] - sq1 <- withStore' c $ \db -> setSndSwitchStatus db sq $ Just SSSendingQKEY + swchStatus <- + if connAgentVersion >= rpcAddressSMPAgentVersion + then do + withStore' c $ \db -> copyPendingSndDeliveries db sq sq2 + enqueueCommand c "" connId (Just $ qServer sq2) $ AInternalCommand $ ICQSndSecure (snd $ qAddress sq2) + pure SSSecuringQueue + else do + let sqInfo' = (sqInfo :: SMPQueueInfo) {queueAddress = queueAddress {dhPublicKey}} + void . enqueueMessages c cData' sqs SMP.noMsgFlags $ QKEY [(sqInfo', C.toPublic sndPrivateKey)] + pure SSSendingQKEY + sq1 <- withStore' c $ \db -> setSndSwitchStatus db sq $ Just swchStatus let sqs'' = updatedQs sq1 sqs' <> [sq2] conn' = DuplexConnection cData' rqs sqs'' cStats <- connectionStats c conn' @@ -3941,6 +3999,23 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(userId, srv, _), THandlePar _ -> qError "QUSE: switching SndQueue not found in connection" _ -> qError "QUSE: switched queue address not found in connection" + -- processed by queue recipient + qEndMsg :: SMP.MsgId -> NonEmpty SndQAddr -> Connection 'CDuplex -> AM () + qEndMsg srvMsgId addrs (DuplexConnection cData'@ConnData {enableNtfs} rqs sqs) = + case L.partition (\rq' -> any (`sameQAddress` sndAddress rq') addrs) rqs of + (removed@(_ : _), keptRq : keptRqs) -> do + logServer "<--" c srv rId $ "MSG :" <> logSecret' srvMsgId + forM_ removed $ \rq'@RcvQueue {server = rmServer, rcvId} -> do + withStore' c $ \db -> setRcvQueueDeleted db rq' + enqueueCommand c "" connId (Just rmServer) $ AInternalCommand $ ICDeleteRcvQueue rcvId + when enableNtfs $ do + ns <- asks ntfSupervisor + liftIO $ sendNtfSubCommand ns (NSCCreate, [connId]) + let conn' = DuplexConnection cData' (keptRq :| keptRqs) sqs + cStats <- connectionStats c conn' + notify $ SWITCH QDRcv SPCompleted cStats + _ -> pure () + qError :: String -> AM a qError = throwE . AGENT . A_QUEUE @@ -3957,15 +4032,10 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(userId, srv, _), THandlePar case conn' of ContactConnection {} -> do -- show connection request even if invitaion via contact address is not compatible. - -- in case invitation not compatible, assume there is no PQ encryption support. - pqSupport <- lift $ maybe PQSupportOff pqSupported <$> compatibleInvitationUri connReq invId <- storeInvitation (CRInvitation connReq) cInfo False let srvs = L.map qServer $ crSmpQueues crData - notify $ REQ invId pqSupport srvs cInfo False + notify $ REQ invId PQSupportOn srvs cInfo False _ -> prohibited "inv: sent to message conn" - where - pqSupported (_, Compatible (CR.E2ERatchetParams v _ _ _), Compatible agentVersion) = - PQSupportOn `CR.pqSupportAnd` versionPQSupport_ agentVersion (Just v) storeInvitation :: ContactRequest -> ConnInfo -> Bool -> AM InvitationId storeInvitation connReq recipientConnInfo serviceRequest = do @@ -3983,15 +4053,15 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(userId, srv, _), THandlePar unlessM duplicateRequest $ withStore' c (\db -> getAddressRatchetKeys db connId ratchetKeyId) >>= \case Right (pk1, pk2, pKem) -> do - (rc, connPQSupport) <- initRcvRatchet_ agentVersion pqSupport (pk1, pk2, pKem) e2eSndParams + (rc, connPQSupport) <- initRcvRatchet_ pqSupport (pk1, pk2, pKem) e2eSndParams (agentMsgBody_, ratchetState) <- decryptConnInfo rc encConnInfo case agentMsgBody_ of Right agentMsgBody -> do let mkDR replyQueue = DRInvitation {ratchetState, replyQueue, agentVersion, pqSupport = connPQSupport} - parseMessage agentMsgBody >>= \case + parseMessage "3" agentMsgBody >>= \case AgentConnInfoReply (replyQueue :| _) cInfo -> do invId <- storeInvitation (CRInvitationDR $ mkDR replyQueue) cInfo False - notify $ REQ invId pqSupported (qServer replyQueue :| []) cInfo True + notify $ REQ invId PQSupportOn (qServer replyQueue :| []) cInfo True AgentServiceRequest (replyQueue :| _) sig_ payload -> case verifyServiceReq rc payload sig_ of Left err -> logError ("service request: " <> T.pack err) >> notify (ERR $ AGENT $ A_SERVICE ASEBadSignature) @@ -4003,9 +4073,6 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(userId, srv, _), THandlePar Left _ -> prohibited "addr inv: unknown ratchetKeyId" _ -> prohibited "inv: sent to message conn" where - pqSupported = case e2eSndParams of - CR.AE2ERatchetParams _ (CR.E2ERatchetParams e2eVersion _ _ _) -> - PQSupportOn `CR.pqSupportAnd` versionPQSupport_ agentVersion (Just e2eVersion) duplicateRequest = case e2eSndParams of CR.AE2ERatchetParams _ (CR.E2ERatchetParams _ k1 k2 _) -> do let rkHash = C.sha256Hash $ C.pubKeyBytes k1 <> C.pubKeyBytes k2 @@ -4053,7 +4120,7 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(userId, srv, _), THandlePar sendReplyKey = do g <- asks random (pks, e2eParams) <- liftIO $ CR.generateRcvE2EParams g e2eVersion pqSupport - enqueueRatchetKeyMsgs c sqs e2eParams + enqueueRatchetKeyMsgs c cData' sqs e2eParams pure pks notifyRatchetSyncError = do let cData'' = cData' {ratchetSyncState = RSRequired} :: ConnData @@ -4195,10 +4262,10 @@ storeConfirmation c cData@ConnData {connId, pqSupport, connAgentVersion = v} sq liftIO $ createSndMsg db connId msgData liftIO $ createSndMsgDelivery db sq internalId -enqueueRatchetKeyMsgs :: AgentClient -> NonEmpty SndQueue -> CR.RcvE2ERatchetParams 'C.X448 -> AM () -enqueueRatchetKeyMsgs c (sq :| sqs) e2eEncryption = do +enqueueRatchetKeyMsgs :: AgentClient -> ConnData -> NonEmpty SndQueue -> CR.RcvE2ERatchetParams 'C.X448 -> AM () +enqueueRatchetKeyMsgs c cData (sq :| sqs) e2eEncryption = do msgId <- enqueueRatchetKey c sq e2eEncryption - mapM_ (lift . enqueueSavedMessage c msgId) $ filter isActiveSndQ sqs + mapM_ (lift . enqueueSavedMessage c msgId) $ filter (isActiveSndQ cData) sqs enqueueRatchetKey :: AgentClient -> SndQueue -> CR.RcvE2ERatchetParams 'C.X448 -> AM AgentMsgId enqueueRatchetKey c sq@SndQueue {connId} e2eEncryption = do @@ -4223,16 +4290,16 @@ enqueueRatchetKey c sq@SndQueue {connId} e2eEncryption = do pure internalId -- encoded AgentMessage -> encoded EncAgentMessage -agentRatchetEncrypt :: DB.Connection -> ConnData -> ByteString -> (VersionSMPA -> PQSupport -> Int) -> Maybe PQEncryption -> CR.VersionE2E -> ExceptT StoreError IO (ByteString, PQEncryption) +agentRatchetEncrypt :: DB.Connection -> ConnData -> ByteString -> (PQSupport -> Int) -> Maybe PQEncryption -> CR.VersionE2E -> ExceptT StoreError IO (ByteString, PQEncryption) agentRatchetEncrypt db cData msg getPaddedLen pqEnc_ currentE2EVersion = do (mek, paddedLen, pqEnc) <- agentRatchetEncryptHeader db cData getPaddedLen pqEnc_ currentE2EVersion encMsg <- withExceptT (SEAgentError . cryptoError) $ CR.rcEncryptMsg mek paddedLen msg pure (encMsg, pqEnc) -agentRatchetEncryptHeader :: DB.Connection -> ConnData -> (VersionSMPA -> PQSupport -> Int) -> Maybe PQEncryption -> CR.VersionE2E -> ExceptT StoreError IO (CR.MsgEncryptKeyX448, Int, PQEncryption) -agentRatchetEncryptHeader db ConnData {connId, connAgentVersion = v, pqSupport} getPaddedLen pqEnc_ currentE2EVersion = do +agentRatchetEncryptHeader :: DB.Connection -> ConnData -> (PQSupport -> Int) -> Maybe PQEncryption -> CR.VersionE2E -> ExceptT StoreError IO (CR.MsgEncryptKeyX448, Int, PQEncryption) +agentRatchetEncryptHeader db ConnData {connId, pqSupport} getPaddedLen pqEnc_ currentE2EVersion = do rc <- ExceptT $ getRatchetForUpdate db connId - let paddedLen = getPaddedLen v pqSupport + let paddedLen = getPaddedLen pqSupport (mek, rc') <- withExceptT (SEAgentError . cryptoError) $ CR.rcEncryptHeader rc pqEnc_ currentE2EVersion liftIO $ updateRatchet db connId rc' CR.SMDNoChange pure (mek, paddedLen, CR.rcSndKEM rc') diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index 43cf19039..b84efb239 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -1950,7 +1950,7 @@ getQueueMessage c rq@RcvQueue {server, rcvId, rcvPrivateKey} = do decryptSMPMessage :: RcvQueue -> SMP.RcvMessage -> AM SMP.ClientRcvMsgBody decryptSMPMessage rq SMP.RcvMessage {msgId, msgBody = SMP.EncRcvMsgBody body} = - liftEither $ parse SMP.clientRcvMsgBodyP (AGENT A_MESSAGE) =<< decrypt body + liftEither $ parse SMP.clientRcvMsgBodyP (AGENT $ A_MESSAGE "decrypt message") =<< decrypt body where decrypt = agentCbDecrypt (rcvDhSecret rq) (C.cbNonce msgId) @@ -2254,7 +2254,7 @@ agentCbDecrypt dhSecret nonce msg = cryptoError :: C.CryptoError -> AgentErrorType cryptoError = \case C.CryptoLargeMsgError -> CMD LARGE "CryptoLargeMsgError" - C.CryptoHeaderError _ -> AGENT A_MESSAGE -- parsing error + C.CryptoHeaderError e -> AGENT $ A_MESSAGE $ "parse msg header " <> e C.CERatchetDuplicateMessage -> AGENT $ A_DUPLICATE Nothing C.AESDecryptError -> c DECRYPT_AES C.CBDecryptError -> c DECRYPT_CB diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index 73926a548..78630b9cb 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -41,12 +41,8 @@ module Simplex.Messaging.Agent.Protocol VersionSMPA, VersionRangeSMPA, pattern VersionSMPA, - duplexHandshakeSMPAgentVersion, - ratchetSyncSMPAgentVersion, - deliveryRcptsSMPAgentVersion, - pqdrSMPAgentVersion, - sndAuthKeySMPAgentVersion, ratchetOnConfSMPAgentVersion, + rpcAddressSMPAgentVersion, currentSMPAgentVersion, supportedSMPAgentVRange, e2eEncConnInfoLength, @@ -304,6 +300,7 @@ import UnliftIO.Exception (Exception) -- 5 - post-quantum double ratchet (3/14/2024) -- 6 - secure reply queues with provided keys (6/14/2024) -- 7 - initialize ratchet on processing confirmation (7/18/2024) +-- 8 - agent RPC and double ratchet PQ encryption from first message to contact address (8/01/2026) data SMPAgentVersion @@ -316,29 +313,20 @@ type VersionRangeSMPA = VersionRange SMPAgentVersion pattern VersionSMPA :: Word16 -> VersionSMPA pattern VersionSMPA v = Version v -duplexHandshakeSMPAgentVersion :: VersionSMPA -duplexHandshakeSMPAgentVersion = VersionSMPA 2 - -ratchetSyncSMPAgentVersion :: VersionSMPA -ratchetSyncSMPAgentVersion = VersionSMPA 3 - -deliveryRcptsSMPAgentVersion :: VersionSMPA -deliveryRcptsSMPAgentVersion = VersionSMPA 4 - -pqdrSMPAgentVersion :: VersionSMPA -pqdrSMPAgentVersion = VersionSMPA 5 - -sndAuthKeySMPAgentVersion :: VersionSMPA -sndAuthKeySMPAgentVersion = VersionSMPA 6 +_sndAuthKeySMPAgentVersion :: VersionSMPA +_sndAuthKeySMPAgentVersion = VersionSMPA 6 ratchetOnConfSMPAgentVersion :: VersionSMPA ratchetOnConfSMPAgentVersion = VersionSMPA 7 +rpcAddressSMPAgentVersion :: VersionSMPA +rpcAddressSMPAgentVersion = VersionSMPA 8 + minSupportedSMPAgentVersion :: VersionSMPA -minSupportedSMPAgentVersion = duplexHandshakeSMPAgentVersion +minSupportedSMPAgentVersion = _sndAuthKeySMPAgentVersion currentSMPAgentVersion :: VersionSMPA -currentSMPAgentVersion = VersionSMPA 7 +currentSMPAgentVersion = VersionSMPA 8 supportedSMPAgentVRange :: VersionRangeSMPA supportedSMPAgentVRange = mkVersionRange minSupportedSMPAgentVersion currentSMPAgentVersion @@ -346,17 +334,17 @@ supportedSMPAgentVRange = mkVersionRange minSupportedSMPAgentVersion currentSMPA -- it is shorter to allow all handshake headers, -- including E2E (double-ratchet) parameters and -- signing key of the sender for the server -e2eEncConnInfoLength :: VersionSMPA -> PQSupport -> Int -e2eEncConnInfoLength v = \case +e2eEncConnInfoLength :: PQSupport -> Int +e2eEncConnInfoLength = \case -- reduced by 3726 (roughly the increase of message ratchet header size + key and ciphertext in reply link) - PQSupportOn | v >= pqdrSMPAgentVersion -> 11106 - _ -> 14832 + PQSupportOn -> 11106 + PQSupportOff -> 14832 -e2eEncAgentMsgLength :: VersionSMPA -> PQSupport -> Int -e2eEncAgentMsgLength v = \case +e2eEncAgentMsgLength :: PQSupport -> Int +e2eEncAgentMsgLength = \case -- reduced by 2222 (the increase of message ratchet header size) - PQSupportOn | v >= pqdrSMPAgentVersion -> 13618 - _ -> 15840 + PQSupportOn -> 13618 + PQSupportOff -> 15840 -- | SMP agent event type ATransmission = (ACorrId, AEntityId, AEvt) @@ -651,16 +639,22 @@ instance FromJSON RcvSwitchStatus where data SndSwitchStatus = SSSendingQKEY | SSSendingQTEST + | SSSecuringQueue + | SSSendingQEND deriving (Eq, Show) instance StrEncoding SndSwitchStatus where strEncode = \case SSSendingQKEY -> "sending_qkey" SSSendingQTEST -> "sending_qtest" + SSSecuringQueue -> "securing_queue" + SSSendingQEND -> "sending_qend" strP = A.takeTill (== ' ') >>= \case "sending_qkey" -> pure SSSendingQKEY "sending_qtest" -> pure SSSendingQTEST + "securing_queue" -> pure SSSecuringQueue + "sending_qend" -> pure SSSendingQEND _ -> fail "bad SndSwitchStatus" instance ToField SndSwitchStatus where toField = toField . decodeLatin1 . strEncode @@ -972,6 +966,7 @@ data AgentMessageType | AM_QKEY_ | AM_QUSE_ | AM_QTEST_ + | AM_QEND_ | AM_EREADY_ | AM_SRV_REQ | AM_SRV_RESP @@ -991,6 +986,7 @@ instance Encoding AgentMessageType where AM_QKEY_ -> "QK" AM_QUSE_ -> "QU" AM_QTEST_ -> "QT" + AM_QEND_ -> "QE" AM_EREADY_ -> "E" AM_SRV_REQ -> "A" AM_SRV_RESP -> "P" @@ -1010,6 +1006,7 @@ instance Encoding AgentMessageType where 'K' -> pure AM_QKEY_ 'U' -> pure AM_QUSE_ 'T' -> pure AM_QTEST_ + 'E' -> pure AM_QEND_ _ -> fail "bad AgentMessageType" 'E' -> pure AM_EREADY_ 'A' -> pure AM_SRV_REQ @@ -1049,6 +1046,7 @@ data AMsgType | QKEY_ | QUSE_ | QTEST_ + | QEND_ | EREADY_ deriving (Eq) @@ -1062,6 +1060,7 @@ instance Encoding AMsgType where QKEY_ -> "QK" QUSE_ -> "QU" QTEST_ -> "QT" + QEND_ -> "QE" EREADY_ -> "E" smpP = A.anyChar >>= \case @@ -1075,6 +1074,7 @@ instance Encoding AMsgType where 'K' -> pure QKEY_ 'U' -> pure QUSE_ 'T' -> pure QTEST_ + 'E' -> pure QEND_ _ -> fail "bad AMsgType" 'E' -> pure EREADY_ _ -> fail "bad AMsgType" @@ -1099,6 +1099,8 @@ data AMessage QUSE (NonEmpty (SndQAddr, Bool)) | -- sent by the sender to test new queues and to complete switching QTEST (NonEmpty SndQAddr) + | -- sent by the sender to remove queues from the connection (fast rotation, v8) + QEND (NonEmpty SndQAddr) | -- ratchet re-synchronization is complete, with last decrypted sender message id (recipient's `last_external_snd_msg_id`) EREADY AgentMsgId deriving (Show) @@ -1117,6 +1119,7 @@ aMessageType = \case QKEY _ -> AM_QKEY_ QUSE _ -> AM_QUSE_ QTEST _ -> AM_QTEST_ + QEND _ -> AM_QEND_ EREADY _ -> AM_EREADY_ -- | this type is used to send as part of the protocol between different clients @@ -1169,6 +1172,7 @@ instance Encoding AMessage where QKEY qs -> smpEncode (QKEY_, qs) QUSE qs -> smpEncode (QUSE_, qs) QTEST qs -> smpEncode (QTEST_, qs) + QEND qs -> smpEncode (QEND_, qs) EREADY lastDecryptedMsgId -> smpEncode (EREADY_, lastDecryptedMsgId) smpP = smpP @@ -1181,6 +1185,7 @@ instance Encoding AMessage where QKEY_ -> QKEY <$> smpP QUSE_ -> QUSE <$> smpP QTEST_ -> QTEST <$> smpP + QEND_ -> QEND <$> smpP EREADY_ -> EREADY <$> smpP instance ToField AMessage where toField = toField . Binary . smpEncode @@ -2195,7 +2200,7 @@ data ConnectionErrorType -- | Errors of another SMP agent. data SMPAgentError = -- | client or agent message that failed to parse - A_MESSAGE + A_MESSAGE {messageErr :: String} | -- | prohibited SMP/agent message A_PROHIBITED {prohibitedErr :: String} | -- | incompatible version of SMP client, agent or encryption protocols diff --git a/src/Simplex/Messaging/Agent/Store.hs b/src/Simplex/Messaging/Agent/Store.hs index abfbe4b47..607e6d1da 100644 --- a/src/Simplex/Messaging/Agent/Store.hs +++ b/src/Simplex/Messaging/Agent/Store.hs @@ -207,12 +207,13 @@ rcvSMPQueueAddress :: RcvQueue -> SMPQueueAddress rcvSMPQueueAddress RcvQueue {server, sndId, e2ePrivKey, queueMode} = SMPQueueAddress server sndId (C.publicKey e2ePrivKey) queueMode -canAbortRcvSwitch :: RcvQueue -> Bool -canAbortRcvSwitch = maybe False canAbort . rcvSwchStatus +canAbortRcvSwitch :: ConnData -> RcvQueue -> Bool +canAbortRcvSwitch ConnData {connAgentVersion} = maybe False canAbort . rcvSwchStatus where canAbort = \case RSSwitchStarted -> True - RSSendingQADD -> True + -- at agent version 8 and above the peer always chooses fast rotation, so a sent QADD is committed + RSSendingQADD -> connAgentVersion < rpcAddressSMPAgentVersion -- if switch is in RSSendingQUSE, a race condition with sender deleting the original queue is possible RSSendingQUSE -> False -- if switch is in RSReceivedMessage status, aborting switch (deleting new queue) @@ -475,8 +476,8 @@ type NoticeId = Int64 -- this function should be mirrored in the clients ratchetSyncAllowed :: ConnData -> Bool -ratchetSyncAllowed ConnData {ratchetSyncState, connAgentVersion} = - connAgentVersion >= ratchetSyncSMPAgentVersion && (ratchetSyncState `elem` ([RSAllowed, RSRequired] :: [RatchetSyncState])) +ratchetSyncAllowed ConnData {ratchetSyncState} = + ratchetSyncState `elem` ([RSAllowed, RSRequired] :: [RatchetSyncState]) -- this function should be mirrored in the clients ratchetSyncSendProhibited :: ConnData -> Bool @@ -538,6 +539,7 @@ data InternalCommand | ICDeleteConn | ICDeleteRcvQueue SMP.RecipientId | ICQSecure SMP.RecipientId SMP.SndPublicAuthKey + | ICQSndSecure SMP.SenderId | ICQDelete SMP.RecipientId | ICReplyDel @@ -549,6 +551,7 @@ data InternalCommandTag | ICDeleteConn_ | ICDeleteRcvQueue_ | ICQSecure_ + | ICQSndSecure_ | ICQDelete_ | ICReplyDel_ deriving (Show) @@ -562,6 +565,7 @@ instance StrEncoding InternalCommand where ICDeleteConn -> strEncode ICDeleteConn_ ICDeleteRcvQueue rId -> strEncode (ICDeleteRcvQueue_, rId) ICQSecure rId senderKey -> strEncode (ICQSecure_, rId, senderKey) + ICQSndSecure sId -> strEncode (ICQSndSecure_, sId) ICQDelete rId -> strEncode (ICQDelete_, rId) ICReplyDel -> strEncode ICReplyDel_ strP = @@ -573,6 +577,7 @@ instance StrEncoding InternalCommand where ICDeleteConn_ -> pure ICDeleteConn ICDeleteRcvQueue_ -> ICDeleteRcvQueue <$> _strP ICQSecure_ -> ICQSecure <$> _strP <*> _strP + ICQSndSecure_ -> ICQSndSecure <$> _strP ICQDelete_ -> ICQDelete <$> _strP ICReplyDel_ -> pure ICReplyDel @@ -585,6 +590,7 @@ instance StrEncoding InternalCommandTag where ICDeleteConn_ -> "DELETE_CONN" ICDeleteRcvQueue_ -> "DELETE_RCV_QUEUE" ICQSecure_ -> "QSECURE" + ICQSndSecure_ -> "QSND_SECURE" ICQDelete_ -> "QDELETE" ICReplyDel_ -> "REPLY_DEL" strP = @@ -596,6 +602,7 @@ instance StrEncoding InternalCommandTag where "DELETE_CONN" -> pure ICDeleteConn_ "DELETE_RCV_QUEUE" -> pure ICDeleteRcvQueue_ "QSECURE" -> pure ICQSecure_ + "QSND_SECURE" -> pure ICQSndSecure_ "QDELETE" -> pure ICQDelete_ "REPLY_DEL" -> pure ICReplyDel_ _ -> fail "bad InternalCommandTag" @@ -614,6 +621,7 @@ internalCmdTag = \case ICDeleteConn -> ICDeleteConn_ ICDeleteRcvQueue {} -> ICDeleteRcvQueue_ ICQSecure {} -> ICQSecure_ + ICQSndSecure {} -> ICQSndSecure_ ICQDelete _ -> ICQDelete_ ICReplyDel -> ICReplyDel_ diff --git a/src/Simplex/Messaging/Agent/Store/AgentStore.hs b/src/Simplex/Messaging/Agent/Store/AgentStore.hs index dacd3c602..04fbcf729 100644 --- a/src/Simplex/Messaging/Agent/Store/AgentStore.hs +++ b/src/Simplex/Messaging/Agent/Store/AgentStore.hs @@ -131,6 +131,8 @@ module Simplex.Messaging.Agent.Store.AgentStore createSndMsg, updateSndMsgHash, createSndMsgDelivery, + copyPendingSndDeliveries, + countSndQueueDeliveries, getSndMsgViaRcpt, updateSndMsgRcpt, getPendingQueueMsg, @@ -1041,6 +1043,24 @@ createSndMsgDelivery :: DB.Connection -> SndQueue -> InternalId -> IO () createSndMsgDelivery db SndQueue {connId, dbQueueId} msgId = DB.execute db "INSERT INTO snd_message_deliveries (conn_id, snd_queue_id, internal_id) VALUES (?, ?, ?)" (connId, dbQueueId, msgId) +-- copies every undelivered (failed = 0) delivery from one snd queue to another, for redundant delivery during fast rotation +copyPendingSndDeliveries :: DB.Connection -> SndQueue -> SndQueue -> IO () +copyPendingSndDeliveries db SndQueue {connId, dbQueueId = fromQueueId} SndQueue {dbQueueId = toQueueId} = + DB.execute + db + [sql| + INSERT INTO snd_message_deliveries (conn_id, snd_queue_id, internal_id) + SELECT conn_id, ?, internal_id + FROM snd_message_deliveries + WHERE conn_id = ? AND snd_queue_id = ? AND failed = 0 + |] + (toQueueId, connId, fromQueueId) + +countSndQueueDeliveries :: DB.Connection -> SndQueue -> IO Int +countSndQueueDeliveries db SndQueue {connId, dbQueueId} = + maybeFirstRow' 0 fromOnly $ + DB.query db "SELECT count(1) FROM snd_message_deliveries WHERE conn_id = ? AND snd_queue_id = ? AND failed = 0" (connId, dbQueueId) + getSndMsgViaRcpt :: DB.Connection -> ConnId -> InternalSndId -> IO (Either StoreError SndMsg) getSndMsgViaRcpt db connId sndMsgId = firstRow toSndMsg (SEMsgNotFound "getSndMsgViaRcpt") $ diff --git a/src/Simplex/Messaging/Crypto/Ratchet.hs b/src/Simplex/Messaging/Crypto/Ratchet.hs index b66b4a519..9373b41f0 100644 --- a/src/Simplex/Messaging/Crypto/Ratchet.hs +++ b/src/Simplex/Messaging/Crypto/Ratchet.hs @@ -51,8 +51,6 @@ module Simplex.Messaging.Crypto.Ratchet VersionRangeE2E, pattern VersionE2E, RatchetVersions (..), - kdfX3DHE2EEncryptVersion, - pqRatchetE2EEncryptVersion, currentE2EEncryptVersion, supportedE2EEncryptVRange, generateRcvE2EParams, @@ -87,8 +85,6 @@ module Simplex.Messaging.Crypto.Ratchet RatchetKey (..), fullHeaderLen, applySMDiff, - encodeMsgHeader, - msgHeaderP, ) where @@ -102,7 +98,6 @@ import Crypto.Random (ChaChaDRG) import Data.Aeson (FromJSON (..), ToJSON (..)) import qualified Data.Aeson as J import qualified Data.Aeson.TH as JQ -import Data.Attoparsec.ByteString (Parser, peekWord8') import qualified Data.Attoparsec.ByteString.Char8 as A import qualified Data.ByteArray as BA import Data.ByteString.Char8 (ByteString) @@ -132,6 +127,7 @@ import UnliftIO.STM -- e2e encryption headers version history: -- 1 - binary protocol encoding (1/1/2022) -- 2 - use KDF in x3dh (10/20/2022) +-- 3 - PQDR (3/14/2024) data E2EVersion @@ -144,17 +140,17 @@ type VersionRangeE2E = VersionRange E2EVersion pattern VersionE2E :: Word16 -> VersionE2E pattern VersionE2E v = Version v -kdfX3DHE2EEncryptVersion :: VersionE2E -kdfX3DHE2EEncryptVersion = VersionE2E 2 +_pqRatchetE2EEncryptVersion :: VersionE2E +_pqRatchetE2EEncryptVersion = VersionE2E 3 -pqRatchetE2EEncryptVersion :: VersionE2E -pqRatchetE2EEncryptVersion = VersionE2E 3 +minSupportedE2EEncryptVersion :: VersionE2E +minSupportedE2EEncryptVersion = _pqRatchetE2EEncryptVersion currentE2EEncryptVersion :: VersionE2E currentE2EEncryptVersion = VersionE2E 3 supportedE2EEncryptVRange :: VersionRangeE2E -supportedE2EEncryptVRange = mkVersionRange kdfX3DHE2EEncryptVersion currentE2EEncryptVersion +supportedE2EEncryptVRange = mkVersionRange minSupportedE2EEncryptVersion currentE2EEncryptVersion data RatchetKEMState = RKSProposed -- only KEM encapsulation key @@ -238,9 +234,7 @@ data AnyE2ERatchetParams deriving instance Show AnyE2ERatchetParams instance (RatchetKEMStateI s, AlgorithmI a) => Encoding (E2ERatchetParams s a) where - smpEncode (E2ERatchetParams v k1 k2 kem_) - | v >= pqRatchetE2EEncryptVersion = smpEncode (v, k1, k2, kem_) - | otherwise = smpEncode (v, k1, k2) + smpEncode (E2ERatchetParams v k1 k2 kem_) = smpEncode (v, k1, k2, kem_) smpP = toParams <$?> smpP where toParams :: AE2ERatchetParams a -> Either String (E2ERatchetParams s a) @@ -261,14 +255,9 @@ instance Encoding AnyE2ERatchetParams where case testEquality a a' of Nothing -> fail "bad e2e params: different key algorithms" Just Refl -> - kemP v >>= \case + smpP >>= \case Just (ARKP s kem) -> pure $ AnyE2ERatchetParams s a $ E2ERatchetParams v k1 k2 (Just kem) Nothing -> pure $ AnyE2ERatchetParams SRKSProposed a $ E2ERatchetParams v k1 k2 Nothing - where - kemP :: VersionE2E -> Parser (Maybe ARKEMParams) - kemP v - | v >= pqRatchetE2EEncryptVersion = smpP - | otherwise = pure Nothing instance VersionI E2EVersion (E2ERatchetParams s a) where type VersionRangeT E2EVersion (E2ERatchetParams s a) = E2ERatchetParamsUri s a @@ -307,11 +296,10 @@ instance (RatchetKEMStateI s, AlgorithmI a) => StrEncoding (E2ERatchetParamsUri [("v", strEncode vs), ("x3dh", strEncodeList [key1, key2])] <> maybe [] encodeKem kem_ where - encodeKem kem - | maxVersion vs < pqRatchetE2EEncryptVersion = [] - | otherwise = case kem of - RKParamsProposed k -> [("kem_key", strEncode k)] - RKParamsAccepted ct k -> [("kem_ct", strEncode ct), ("kem_key", strEncode k)] + encodeKem :: RKEMParams s -> [(ByteString, ByteString)] + encodeKem kem = case kem of + RKParamsProposed k -> [("kem_key", strEncode k)] + RKParamsAccepted ct k -> [("kem_ct", strEncode ct), ("kem_key", strEncode k)] strP = toE2ERatchetParamsUri <$?> strP {-# INLINE strP #-} @@ -328,25 +316,26 @@ instance StrEncoding AnyE2ERatchetParamsUri where strEncode (AnyE2ERatchetParamsUri _ _ ps) = strEncode ps strP = do query <- strP - vr :: VersionRangeE2E <- queryParam "v" query + vr :: VersionRangeE2E <- adjustE2EVRange <$> queryParam "v" query keys <- L.toList <$> queryParam "x3dh" query case keys of [APublicDhKey a k1, APublicDhKey a' k2] -> case testEquality a a' of Nothing -> fail "bad e2e params: different key algorithms" Just Refl -> - kemP vr query >>= \case + kemP query >>= \case Just (ARKP s kem) -> pure $ AnyE2ERatchetParamsUri s a $ E2ERatchetParamsUri vr k1 k2 (Just kem) Nothing -> pure $ AnyE2ERatchetParamsUri SRKSProposed a $ E2ERatchetParamsUri vr k1 k2 Nothing _ -> fail "bad e2e params" where - kemP vr query - | maxVersion vr >= pqRatchetE2EEncryptVersion = - queryParam_ "kem_key" query - $>>= \k -> Just . kemParams k <$> queryParam_ "kem_ct" query - | otherwise = pure Nothing + kemP query = + queryParam_ "kem_key" query + $>>= \k -> Just . kemParams k <$> queryParam_ "kem_ct" query kemParams k = \case Nothing -> ARKP SRKSProposed $ RKParamsProposed k Just ct -> ARKP SRKSAccepted $ RKParamsAccepted ct k + adjustE2EVRange vr = + let v = max minSupportedE2EEncryptVersion $ minVersion vr + in fromMaybe vr $ safeVersionRange v (max v $ maxVersion vr) instance (RatchetKEMStateI s, AlgorithmI a) => Encoding (E2ERatchetParamsUri s a) where smpEncode (E2ERatchetParamsUri vr k1 k2 kem_) = smpEncode (vr, k1, k2, kem_) @@ -432,16 +421,14 @@ generateE2EParams g v useKEM_ = do where kemParams :: IO (Maybe (RKEMParams s, PrivRKEMParams s)) kemParams = case useKEM_ of - Just useKem - | v >= pqRatchetE2EEncryptVersion -> - Just <$> do - ks@(k, _) <- sntrup761Keypair g - case useKem of - ProposeKEM -> pure (RKParamsProposed k, PrivateRKParamsProposed ks) - AcceptKEM k' -> do - (ct, shared) <- sntrup761Enc g k' - pure (RKParamsAccepted ct k, PrivateRKParamsAccepted ct shared ks) - _ -> pure Nothing + Just useKem -> Just <$> do + ks@(k, _) <- sntrup761Keypair g + case useKem of + ProposeKEM -> pure (RKParamsProposed k, PrivateRKParamsProposed ks) + AcceptKEM k' -> do + (ct, shared) <- sntrup761Enc g k' + pure (RKParamsAccepted ct k, PrivateRKParamsAccepted ct shared ks) + Nothing -> pure Nothing -- used by party initiating connection, Bob in double-ratchet spec generateRcvE2EParams :: (AlgorithmI a, DhAlgorithm a) => TVar ChaChaDRG -> VersionE2E -> PQSupport -> IO (RcvE2EPrivRatchetParams a, RcvE2ERatchetParams a) @@ -474,30 +461,30 @@ data RatchetInitParams = RatchetInitParams -- this is used by the peer joining the connection pqX3dhSnd :: DhAlgorithm a => AE2EPrivRatchetParams a -> E2ERatchetParams 'RKSProposed a -> Either CryptoError (RatchetInitParams, Maybe KEMKeyPair) -- 3. replied 2. received -pqX3dhSnd (spk1, spk2, spKem_) (E2ERatchetParams v rk1 rk2 rKem_) = do +pqX3dhSnd (spk1, spk2, spKem_) (E2ERatchetParams _ rk1 rk2 rKem_) = do (ks_, kem_) <- sndPq let initParams = pqX3dh (publicKey spk1, rk1) (dh' rk1 spk2) (dh' rk2 spk1) (dh' rk2 spk2) kem_ pure (initParams, ks_) where sndPq :: Either CryptoError (Maybe KEMKeyPair, Maybe RatchetKEMAccepted) sndPq = case spKem_ of - Just (APRKP _ ps) | v >= pqRatchetE2EEncryptVersion -> case (ps, rKem_) of + Just (APRKP _ ps) -> case (ps, rKem_) of (PrivateRKParamsAccepted ct shared ks, Just (RKParamsProposed k)) -> Right (Just ks, Just $ RatchetKEMAccepted k shared ct) (PrivateRKParamsProposed ks, _) -> Right (Just ks, Nothing) -- both parties can send "proposal" in case of ratchet renegotiation _ -> Left CERatchetKEMState - _ -> Right (Nothing, Nothing) + Nothing -> Right (Nothing, Nothing) -- this is used by the peer that created new connection, after receiving the reply pqX3dhRcv :: forall s a. (RatchetKEMStateI s, DhAlgorithm a) => RcvE2EPrivRatchetParams a -> E2ERatchetParams s a -> ExceptT CryptoError IO (RatchetInitParams, Maybe KEMKeyPair) -- 1. sent 4. received in reply -pqX3dhRcv (rpk1, rpk2, rpKem_) (E2ERatchetParams v sk1 sk2 sKem_) = do +pqX3dhRcv (rpk1, rpk2, rpKem_) (E2ERatchetParams _ sk1 sk2 sKem_) = do kem_ <- rcvPq let initParams = pqX3dh (sk1, publicKey rpk1) (dh' sk2 rpk1) (dh' sk1 rpk2) (dh' sk2 rpk2) (snd <$> kem_) pure (initParams, fst <$> kem_) where rcvPq :: ExceptT CryptoError IO (Maybe (KEMKeyPair, RatchetKEMAccepted)) rcvPq = case sKem_ of - Just (RKParamsAccepted ct k') | v >= pqRatchetE2EEncryptVersion -> case rpKem_ of + Just (RKParamsAccepted ct k') -> case rpKem_ of Just (PrivateRKParamsProposed ks@(_, pk)) -> do shared <- liftIO $ sntrup761Dec ct pk pure $ Just (ks, RatchetKEMAccepted k' shared ct) @@ -721,31 +708,22 @@ data MsgHeader a = MsgHeader -- to allow extension without increasing the size, the actual header length is: -- 69 = 2 (original size) + 2 + 1+56 (Curve448) + 4 + 4 -- The exact size is 2288, added reserve -paddedHeaderLen :: VersionE2E -> PQSupport -> Int -paddedHeaderLen v = \case - PQSupportOn | v >= pqRatchetE2EEncryptVersion -> 2310 - _ -> 88 +paddedHeaderLen :: PQSupport -> Int +paddedHeaderLen = \case + PQSupportOn -> 2310 + PQSupportOff -> 88 -- only used in tests to validate correct padding -- (2 bytes - version size, 1 byte - header size) -fullHeaderLen :: VersionE2E -> PQSupport -> Int -fullHeaderLen v pq = 2 + 1 + paddedHeaderLen v pq + authTagSize + ivSize @AES256 +fullHeaderLen :: PQSupport -> Int +fullHeaderLen pq = 2 + 1 + paddedHeaderLen pq + authTagSize + ivSize @AES256 --- pass the current version, as MsgHeader only includes the max supported version that can be different from the current -encodeMsgHeader :: AlgorithmI a => VersionE2E -> MsgHeader a -> ByteString -encodeMsgHeader v MsgHeader {msgMaxVersion, msgDHRs, msgKEM, msgPN, msgNs} - | v >= pqRatchetE2EEncryptVersion = smpEncode (msgMaxVersion, msgDHRs, msgKEM, msgPN, msgNs) - | otherwise = smpEncode (msgMaxVersion, msgDHRs, msgPN, msgNs) - --- pass the current version, as MsgHeader only includes the max supported version that can be different from the current -msgHeaderP :: AlgorithmI a => VersionE2E -> Parser (MsgHeader a) -msgHeaderP v = do - msgMaxVersion <- smpP - msgDHRs <- smpP - msgKEM <- if v >= pqRatchetE2EEncryptVersion then smpP else pure Nothing - msgPN <- smpP - msgNs <- smpP - pure MsgHeader {msgMaxVersion, msgDHRs, msgKEM, msgPN, msgNs} +instance AlgorithmI a => Encoding (MsgHeader a) where + smpEncode MsgHeader {msgMaxVersion, msgDHRs, msgKEM, msgPN, msgNs} = + smpEncode (msgMaxVersion, msgDHRs, msgKEM, msgPN, msgNs) + smpP = do + (msgMaxVersion, msgDHRs, msgKEM, msgPN, msgNs) <- smpP + pure MsgHeader {msgMaxVersion, msgDHRs, msgKEM, msgPN, msgNs} data EncMessageHeader = EncMessageHeader { ehVersion :: VersionE2E, -- this is current ratchet version @@ -757,26 +735,11 @@ data EncMessageHeader = EncMessageHeader -- this encoding depends on version in EncMessageHeader because it is "current" ratchet version instance Encoding EncMessageHeader where smpEncode EncMessageHeader {ehVersion, ehIV, ehAuthTag, ehBody} = - smpEncode (ehVersion, ehIV, ehAuthTag) <> encodeLarge ehVersion ehBody + smpEncode (ehVersion, ehIV, ehAuthTag, Large ehBody) smpP = do - (ehVersion, ehIV, ehAuthTag) <- smpP - ehBody <- largeP + (ehVersion, ehIV, ehAuthTag, Large ehBody) <- smpP pure EncMessageHeader {ehVersion, ehIV, ehAuthTag, ehBody} --- the encoder always uses 2-byte lengths for the new version, even for short headers without PQ keys. -encodeLarge :: VersionE2E -> ByteString -> ByteString -encodeLarge v s - | v >= pqRatchetE2EEncryptVersion = smpEncode $ Large s - | otherwise = smpEncode s - --- This parser relies on the fact that header cannot be shorter than 32 bytes (it is ~69 bytes without PQ KEM), --- therefore if the first byte is less or equal to 31 (x1F), then we have 2 byte-length limited to 8191. --- This allows upgrading the current version in one message. -largeP :: Parser ByteString -largeP = do - len1 <- peekWord8' - if len1 < 32 then unLarge <$> smpP else smpP - -- the header is length-prefixed to parse it as string and use as part of associated data for authenticated encryption data EncRatchetMessage = EncRatchetMessage { emHeader :: ByteString, @@ -784,15 +747,12 @@ data EncRatchetMessage = EncRatchetMessage emBody :: ByteString } -encodeEncRatchetMessage :: VersionE2E -> EncRatchetMessage -> ByteString -encodeEncRatchetMessage v EncRatchetMessage {emHeader, emBody, emAuthTag} = - encodeLarge v emHeader <> smpEncode (emAuthTag, Tail emBody) - -encRatchetMessageP :: Parser EncRatchetMessage -encRatchetMessageP = do - emHeader <- largeP - (emAuthTag, Tail emBody) <- smpP - pure EncRatchetMessage {emHeader, emBody, emAuthTag} +instance Encoding EncRatchetMessage where + smpEncode EncRatchetMessage {emHeader, emBody, emAuthTag} = + smpEncode (Large emHeader, emAuthTag, Tail emBody) + smpP = do + (Large emHeader, emAuthTag, Tail emBody) <- smpP + pure EncRatchetMessage {emHeader, emBody, emAuthTag} newtype PQEncryption = PQEncryption {enablePQ :: Bool} deriving (Eq, Show) @@ -841,15 +801,15 @@ pqEncToSupport (PQEncryption pq) = PQSupport pq pqSupportAnd :: PQSupport -> PQSupport -> PQSupport pqSupportAnd (PQSupport s1) (PQSupport s2) = PQSupport $ s1 && s2 -pqEnableSupport :: VersionE2E -> PQSupport -> PQEncryption -> PQSupport -pqEnableSupport v (PQSupport sup) (PQEncryption enc) = PQSupport $ sup || (v >= pqRatchetE2EEncryptVersion && enc) +pqEnableSupport :: PQSupport -> PQEncryption -> PQSupport +pqEnableSupport (PQSupport sup) (PQEncryption enc) = PQSupport $ sup || enc -replyKEM_ :: VersionE2E -> Maybe (RKEMParams 'RKSProposed) -> PQSupport -> Maybe AUseKEM -replyKEM_ v kem_ = \case - PQSupportOn | v >= pqRatchetE2EEncryptVersion -> Just $ case kem_ of +replyKEM_ :: Maybe (RKEMParams 'RKSProposed) -> PQSupport -> Maybe AUseKEM +replyKEM_ kem_ = \case + PQSupportOn -> Just $ case kem_ of Just (RKParamsProposed k) -> AUseKEM SRKSAccepted $ AcceptKEM k Nothing -> AUseKEM SRKSProposed ProposeKEM - _ -> Nothing + PQSupportOff -> Nothing instance StrEncoding PQEncryption where strEncode pqMode @@ -898,9 +858,9 @@ connPQEncryption = \case IKUsePQ -> PQSupportOn IKLinkPQ pq -> pq -- default for creating connection is IKLinkPQ PQEncOn -joinContactInitialKeys :: Bool -> PQSupport -> InitialKeys -joinContactInitialKeys pqCompatible = \case - PQSupportOn | pqCompatible -> IKUsePQ +joinContactInitialKeys :: PQSupport -> InitialKeys +joinContactInitialKeys = \case + PQSupportOn -> IKUsePQ pqEnc -> IKLinkPQ pqEnc rcCheckCanPad :: Int -> ByteString -> ExceptT CryptoError IO () @@ -916,14 +876,14 @@ rcEncryptHeader rc@Ratchet {rcSnd = Just sr@SndRatchet {rcCKs, rcHKs}, rcDHRs, r -- PQ encryption can be enabled or disabled rcEnableKEM' = fromMaybe rcEnableKEM pqEnc_ -- support for PQ encryption (and therefore large headers/small envelopes) can only be enabled, it cannot be disabled - rcSupportKEM' = pqEnableSupport v rcSupportKEM rcEnableKEM' + rcSupportKEM' = pqEnableSupport rcSupportKEM rcEnableKEM' -- This sets max version to support PQ encryption. -- Current version upgrade happens when peer decrypts the message. -- TODO note that maxSupported will not downgrade here below current (v). - maxSupported' = max supportedE2EVersion $ if pqEnc_ == Just PQEncOn then pqRatchetE2EEncryptVersion else v + maxSupported' = max supportedE2EVersion $ if pqEnc_ == Just PQEncOn then minSupportedE2EEncryptVersion else v rcVersion' = rcVersion {maxSupported = maxSupported'} -- enc_header = HENCRYPT(state.HKs, header) - (ehAuthTag, ehBody) <- encryptAEAD rcHKs ehIV (paddedHeaderLen v rcSupportKEM') rcAD (msgHeader v maxSupported') + (ehAuthTag, ehBody) <- encryptAEAD rcHKs ehIV (paddedHeaderLen rcSupportKEM') rcAD (msgHeader maxSupported') -- return enc_header let emHeader = smpEncode EncMessageHeader {ehVersion = v, ehBody, ehAuthTag, ehIV} msgEncryptKey = @@ -951,9 +911,8 @@ rcEncryptHeader rc@Ratchet {rcSnd = Just sr@SndRatchet {rcCKs, rcHKs}, rcDHRs, r -- pn = state.PN, -- n = state.Ns -- ) - msgHeader v maxSupported' = - encodeMsgHeader - v + msgHeader maxSupported' = + smpEncode MsgHeader { msgMaxVersion = maxSupported', msgDHRs = publicKey rcDHRs, @@ -976,11 +935,10 @@ data MsgEncryptKey a = MsgEncryptKey deriving (Show) rcEncryptMsg :: AlgorithmI a => MsgEncryptKey a -> Int -> ByteString -> ExceptT CryptoError IO ByteString -rcEncryptMsg MsgEncryptKey {msgKey = MessageKey mk iv, msgRcAD, msgEncHeader, msgRcVersion = v} paddedMsgLen msg = do +rcEncryptMsg MsgEncryptKey {msgKey = MessageKey mk iv, msgRcAD, msgEncHeader} paddedMsgLen msg = do -- return ENCRYPT(mk, plaintext, CONCAT(AD, enc_header)) (emAuthTag, emBody) <- encryptAEAD mk iv paddedMsgLen (msgRcAD <> msgEncHeader) msg - let msg' = encodeEncRatchetMessage v EncRatchetMessage {emHeader = msgEncHeader, emBody, emAuthTag} - pure msg' + pure $ smpEncode EncRatchetMessage {emHeader = msgEncHeader, emBody, emAuthTag} data SkippedMessage a = SMMessage (DecryptResult a) @@ -1004,7 +962,7 @@ rcDecrypt :: ByteString -> ExceptT CryptoError IO (DecryptResult a) rcDecrypt g rc@Ratchet {rcRcv, rcAD = Str rcAD, rcVersion} rcMKSkipped msg' = do - encMsg@EncRatchetMessage {emHeader} <- parseE CryptoHeaderError encRatchetMessageP msg' + encMsg@EncRatchetMessage {emHeader} <- parseE CryptoHeaderError smpP msg' encHdr <- parseE CryptoHeaderError smpP emHeader -- plaintext = TrySkippedMessageKeysHE(state, enc_header, cipher-text, AD) decryptSkipped encHdr encMsg >>= \case @@ -1049,7 +1007,7 @@ rcDecrypt g rc@Ratchet {rcRcv, rcAD = Str rcAD, rcVersion} rcMKSkipped msg' = do smkDiff :: SkippedMsgKeys -> SkippedMsgDiff smkDiff smks = if M.null smks then SMDNoChange else SMDAdd smks ratchetStep :: Ratchet a -> MsgHeader a -> ExceptT CryptoError IO (Ratchet a) - ratchetStep rc'@Ratchet {rcDHRs, rcRK, rcNHKs, rcNHKr, rcSupportKEM, rcVersion = rv} MsgHeader {msgDHRs, msgKEM} = do + ratchetStep rc'@Ratchet {rcDHRs, rcRK, rcNHKs, rcNHKr, rcSupportKEM} MsgHeader {msgDHRs, msgKEM} = do (kemSS, kemSS', rcKEM') <- pqRatchetStep rc' msgKEM -- state.DHRs = GENERATE_DH() (_, rcDHRs') <- atomically $ generateKeyPair @a g @@ -1064,7 +1022,7 @@ rcDecrypt g rc@Ratchet {rcRcv, rcAD = Str rcAD, rcVersion} rcMKSkipped msg' = do rc' { rcDHRs = rcDHRs', rcKEM = rcKEM', - rcSupportKEM = pqEnableSupport (current rv) rcSupportKEM rcEnableKEM', + rcSupportKEM = pqEnableSupport rcSupportKEM rcEnableKEM', rcEnableKEM = rcEnableKEM', rcSndKEM = PQEncryption sndKEM, rcRcvKEM = PQEncryption rcvKEM, @@ -1078,17 +1036,17 @@ rcDecrypt g rc@Ratchet {rcRcv, rcAD = Str rcAD, rcVersion} rcMKSkipped msg' = do rcNHKr = rcNHKr' } pqRatchetStep :: Ratchet a -> Maybe ARKEMParams -> ExceptT CryptoError IO (Maybe KEMSharedKey, Maybe KEMSharedKey, Maybe RatchetKEM) - pqRatchetStep Ratchet {rcKEM, rcEnableKEM = PQEncryption pqEnc, rcVersion = rv} = \case + pqRatchetStep Ratchet {rcKEM, rcEnableKEM = PQEncryption pqEnc} = \case -- received message does not have KEM in header, -- but the user enabled KEM when sending previous message Nothing -> case rcKEM of - Nothing | pqEnc && current rv >= pqRatchetE2EEncryptVersion -> do + Nothing | pqEnc -> do rcPQRs <- liftIO $ sntrup761Keypair g pure (Nothing, Nothing, Just RatchetKEM {rcPQRs, rcKEMs = Nothing}) _ -> pure (Nothing, Nothing, Nothing) -- received message has KEM in header. Just (ARKP _ ps) - | pqEnc && current rv >= pqRatchetE2EEncryptVersion -> do + | pqEnc -> do -- state.PQRr = header.kem (ss, rcPQRr) <- sharedSecret -- state.PQRct = PQKEM-ENC(state.PQRr, state.PQRss) // encapsulated additional shared secret KEM #1 @@ -1156,9 +1114,9 @@ rcDecrypt g rc@Ratchet {rcRcv, rcAD = Str rcAD, rcVersion} rcMKSkipped msg' = do e -> throwE e -- header = HDECRYPT(state.NHKr, enc_header) decryptNextHeader hdr = (AdvanceRatchet,) <$> decryptHeader (rcNHKr rc) hdr - decryptHeader k EncMessageHeader {ehVersion, ehBody, ehAuthTag, ehIV} = do + decryptHeader k EncMessageHeader {ehBody, ehAuthTag, ehIV} = do header <- decryptAEAD k ehIV rcAD ehBody ehAuthTag `catchE` \_ -> throwE CERatchetHeader - parseE' CryptoHeaderError (msgHeaderP ehVersion) header + 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)) diff --git a/src/Simplex/Messaging/Crypto/ShortLink.hs b/src/Simplex/Messaging/Crypto/ShortLink.hs index af365ebfe..840366543 100644 --- a/src/Simplex/Messaging/Crypto/ShortLink.hs +++ b/src/Simplex/Messaging/Crypto/ShortLink.hs @@ -120,6 +120,6 @@ decryptLinkData linkKey k (encFD, encMD) = do pure (sig, s) decode :: Encoding a => ByteString -> Either AgentErrorType a decode = msgErr . smpDecode - msgErr = first (const $ AGENT A_MESSAGE) + msgErr = first (const $ AGENT $ A_MESSAGE "parse link data") linkErr :: String -> Either AgentErrorType () linkErr = Left . AGENT . A_LINK diff --git a/tests/AgentTests/ConnectionRequestTests.hs b/tests/AgentTests/ConnectionRequestTests.hs index d0f5ee9df..c3b255139 100644 --- a/tests/AgentTests/ConnectionRequestTests.hs +++ b/tests/AgentTests/ConnectionRequestTests.hs @@ -26,7 +26,7 @@ import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.Ratchet import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Protocol (EntityId (..), ProtocolServer (..), QueueMode (..), SubscriptionMode (..), currentSMPClientVersion, supportedSMPClientVRange, pattern VersionSMPC) +import Simplex.Messaging.Protocol (EntityId (..), ProtocolServer (..), QueueMode (..), currentSMPClientVersion, supportedSMPClientVRange, pattern VersionSMPC) import Simplex.Messaging.ServiceScheme (ServiceScheme (..)) import Simplex.Messaging.Version import Test.Hspec hiding (fit, it) @@ -146,8 +146,8 @@ connReqData1 = connReqData {crSmpQueues = [queue1]} connReqDataV1 :: ConnReqUriData connReqDataV1 = connReqData {crAgentVRange = mkVersionRange (VersionSMPA 1) (VersionSMPA 1)} -connReqDataV2 :: ConnReqUriData -connReqDataV2 = connReqData {crAgentVRange = mkVersionRange (VersionSMPA 2) (VersionSMPA 2)} +connReqDataV6 :: ConnReqUriData +connReqDataV6 = connReqData {crAgentVRange = mkVersionRange (VersionSMPA 6) (VersionSMPA 6)} connReqDataNew :: ConnReqUriData connReqDataNew = connReqData {crSmpQueues = [queueNew]} @@ -159,10 +159,10 @@ testDhPubKey :: C.PublicKeyX448 testDhPubKey = "MEIwBQYDK2VvAzkAmKuSYeQ/m0SixPDS8Wq8VBaTS1cW+Lp0n0h4Diu+kUpR+qXx4SDJ32YGEFoGFGSbGPry5Ychr6U=" testE2ERatchetParams :: RcvE2ERatchetParamsUri 'C.X448 -testE2ERatchetParams = E2ERatchetParamsUri (mkVersionRange (VersionE2E 1) (VersionE2E 1)) testDhPubKey testDhPubKey Nothing +testE2ERatchetParams = E2ERatchetParamsUri (mkVersionRange (VersionE2E 3) (VersionE2E 3)) testDhPubKey testDhPubKey Nothing testE2ERatchetParamsStrUri :: ByteString -testE2ERatchetParamsStrUri = "v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D" +testE2ERatchetParamsStrUri = "v%3D3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D" testE2ERatchetParams12 :: RcvE2ERatchetParamsUri 'C.X448 testE2ERatchetParams12 = E2ERatchetParamsUri supportedE2EEncryptVRange testDhPubKey testDhPubKey Nothing @@ -200,8 +200,8 @@ contactConnRequest = CRContactUri connReqData Nothing contactAddressDR :: AConnectionRequestUri contactAddressDR = ACR SCMContact $ CRContactUri connReqData (Just (RatchetKeyId "0123456789abcdef", testE2ERatchetParams)) -contactAddressV2 :: AConnectionRequestUri -contactAddressV2 = ACR SCMContact $ CRContactUri connReqDataV2 Nothing +contactAddressV6 :: AConnectionRequestUri +contactAddressV6 = ACR SCMContact $ CRContactUri connReqDataV6 Nothing contactAddressNew :: AConnectionRequestUri contactAddressNew = ACR SCMContact $ CRContactUri connReqDataNew Nothing @@ -264,27 +264,27 @@ connectionRequestTests = queueV1NoPort #== ("smp://1234-w==@smp.simplex.im/3456-w==#/?v=1-1&dh=" <> url testDhKeyStr <> "&srv=jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion") queueV1NoPort #== ("smp://1234-w==@smp.simplex.im,jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion/3456-w==#" <> testDhKeyStr) it "should serialize and parse connection invitations and contact addresses" $ do - connectionRequest #==# ("simplex:/invitation#/?v=2-7&smp=" <> url queueStr <> "&e2e=" <> testE2ERatchetParamsStrUri) - connectionRequest #== ("https://simplex.chat/invitation#/?v=2-7&smp=" <> url queueStr <> "&e2e=" <> testE2ERatchetParamsStrUri) - connectionRequestNoQM #==# ("simplex:/invitation#/?v=2-7&smp=" <> url queueStrNoQM <> "&e2e=" <> testE2ERatchetParamsStrUri) - connectionRequest1 #==# ("simplex:/invitation#/?v=2-7&smp=" <> url queue1Str <> "&e2e=" <> testE2ERatchetParamsStrUri) - connectionRequest2queues #==# ("simplex:/invitation#/?v=2-7&smp=" <> url (queueStr <> ";" <> queueStr) <> "&e2e=" <> testE2ERatchetParamsStrUri) - connectionRequestNew #==# ("simplex:/invitation#/?v=2-7&smp=" <> url queueNewStr <> "&e2e=" <> testE2ERatchetParamsStrUri) - connectionRequestNew1 #==# ("simplex:/invitation#/?v=2-7&smp=" <> url queueNew1Str <> "&e2e=" <> testE2ERatchetParamsStrUri) - connectionRequest2queuesNew #==# ("simplex:/invitation#/?v=2-7&smp=" <> url (queueNewStr <> ";" <> queueNewStr) <> "&e2e=" <> testE2ERatchetParamsStrUri) + connectionRequest #==# ("simplex:/invitation#/?v=6-8&smp=" <> url queueStr <> "&e2e=" <> testE2ERatchetParamsStrUri) + connectionRequest #== ("https://simplex.chat/invitation#/?v=6-8&smp=" <> url queueStr <> "&e2e=" <> testE2ERatchetParamsStrUri) + connectionRequestNoQM #==# ("simplex:/invitation#/?v=6-8&smp=" <> url queueStrNoQM <> "&e2e=" <> testE2ERatchetParamsStrUri) + connectionRequest1 #==# ("simplex:/invitation#/?v=6-8&smp=" <> url queue1Str <> "&e2e=" <> testE2ERatchetParamsStrUri) + connectionRequest2queues #==# ("simplex:/invitation#/?v=6-8&smp=" <> url (queueStr <> ";" <> queueStr) <> "&e2e=" <> testE2ERatchetParamsStrUri) + connectionRequestNew #==# ("simplex:/invitation#/?v=6-8&smp=" <> url queueNewStr <> "&e2e=" <> testE2ERatchetParamsStrUri) + connectionRequestNew1 #==# ("simplex:/invitation#/?v=6-8&smp=" <> url queueNew1Str <> "&e2e=" <> testE2ERatchetParamsStrUri) + connectionRequest2queuesNew #==# ("simplex:/invitation#/?v=6-8&smp=" <> url (queueNewStr <> ";" <> queueNewStr) <> "&e2e=" <> testE2ERatchetParamsStrUri) connectionRequestV1 #== ("https://simplex.chat/invitation#/?v=1&smp=" <> url queueStr <> "&e2e=" <> testE2ERatchetParamsStrUri) - connectionRequestClientDataEmpty #==# ("simplex:/invitation#/?v=2-7&smp=" <> url queueStr <> "&e2e=" <> testE2ERatchetParamsStrUri <> "&data=" <> url "{}") - contactAddress #==# ("simplex:/contact#/?v=2-7&smp=" <> url queueStr) - contactAddressDR #==# ("simplex:/contact#/?v=2-7&smp=" <> url queueStr <> "&e2e=" <> testE2ERatchetParamsStrUri <> "&rk=MDEyMzQ1Njc4OWFiY2RlZg%3D%3D") - contactAddress #== ("https://simplex.chat/contact#/?v=2-7&smp=" <> url queueStr) - contactAddress2queues #==# ("simplex:/contact#/?v=2-7&smp=" <> url (queueStr <> ";" <> queueStr)) - contactAddressNew #==# ("simplex:/contact#/?v=2-7&smp=" <> url queueNewStr) - contactAddress2queuesNew #==# ("simplex:/contact#/?v=2-7&smp=" <> url (queueNewStr <> ";" <> queueNewStr)) - contactAddressV2 #==# ("simplex:/contact#/?v=2&smp=" <> url queueStr) - contactAddressV2 #== ("https://simplex.chat/contact#/?v=1&smp=" <> url queueStr) -- adjusted to v2 - contactAddressV2 #== ("https://simplex.chat/contact#/?v=1-2&smp=" <> url queueStr) -- adjusted to v2 - contactAddressV2 #== ("https://simplex.chat/contact#/?v=2-2&smp=" <> url queueStr) - contactAddressClientData #==# ("simplex:/contact#/?v=2-7&smp=" <> url queueStr <> "&data=" <> url "{\"type\":\"group_link\", \"group_link_id\":\"abc\"}") + connectionRequestClientDataEmpty #==# ("simplex:/invitation#/?v=6-8&smp=" <> url queueStr <> "&e2e=" <> testE2ERatchetParamsStrUri <> "&data=" <> url "{}") + contactAddress #==# ("simplex:/contact#/?v=6-8&smp=" <> url queueStr) + contactAddressDR #==# ("simplex:/contact#/?v=6-8&smp=" <> url queueStr <> "&e2e=" <> testE2ERatchetParamsStrUri <> "&rk=MDEyMzQ1Njc4OWFiY2RlZg%3D%3D") + contactAddress #== ("https://simplex.chat/contact#/?v=6-8&smp=" <> url queueStr) + contactAddress2queues #==# ("simplex:/contact#/?v=6-8&smp=" <> url (queueStr <> ";" <> queueStr)) + contactAddressNew #==# ("simplex:/contact#/?v=6-8&smp=" <> url queueNewStr) + contactAddress2queuesNew #==# ("simplex:/contact#/?v=6-8&smp=" <> url (queueNewStr <> ";" <> queueNewStr)) + contactAddressV6 #==# ("simplex:/contact#/?v=6&smp=" <> url queueStr) + contactAddressV6 #== ("https://simplex.chat/contact#/?v=1&smp=" <> url queueStr) -- adjusted to v6 + contactAddressV6 #== ("https://simplex.chat/contact#/?v=1-2&smp=" <> url queueStr) -- adjusted to v6 + contactAddressV6 #== ("https://simplex.chat/contact#/?v=2-2&smp=" <> url queueStr) + contactAddressClientData #==# ("simplex:/contact#/?v=6-8&smp=" <> url queueStr <> "&data=" <> url "{\"type\":\"group_link\", \"group_link_id\":\"abc\"}") it "should serialize / parse queue address, connection invitations and contact addresses as binary" $ do smpEncodingTest queue smpEncodingTest queueNoQM -- this passes, no queue mode patch in SMPQueueUri encoding @@ -309,7 +309,7 @@ connectionRequestTests = smpEncodingTest (aBinaryConnReq contactAddress2queues) smpEncodingTest (aBinaryConnReq contactAddressNew) smpEncodingTest (aBinaryConnReq contactAddress2queuesNew) - smpEncodingTest (aBinaryConnReq contactAddressV2) + smpEncodingTest (aBinaryConnReq contactAddressV6) smpEncodingTest (aBinaryConnReq contactAddressClientData) it "should serialize / parse short links" $ do CSLContact SLSServer CCTContact srv (LinkKey "0123456789abcdef0123456789abcdef") #==# "https://smp.simplex.im/a#MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY?h=jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&p=5223&c=1234-w" diff --git a/tests/AgentTests/DoubleRatchetTests.hs b/tests/AgentTests/DoubleRatchetTests.hs index 5c7ce80d5..95bfb67d4 100644 --- a/tests/AgentTests/DoubleRatchetTests.hs +++ b/tests/AgentTests/DoubleRatchetTests.hs @@ -39,8 +39,7 @@ doubleRatchetTests :: Spec doubleRatchetTests = do describe "double-ratchet encryption/decryption" $ do it "should serialize and parse message header" $ do - testAlgs $ testMessageHeader kdfX3DHE2EEncryptVersion - testAlgs $ testMessageHeader $ max pqRatchetE2EEncryptVersion currentE2EEncryptVersion + testAlgs $ testMessageHeader currentE2EEncryptVersion describe "message tests" $ runMessageTests initRatchets False it "should encode/decode ratchet as JSON" $ do testAlgs testKeyJSON @@ -90,18 +89,15 @@ paddedMsgLen :: Int paddedMsgLen = 100 fullMsgLen :: Ratchet a -> Int -fullMsgLen Ratchet {rcSupportKEM, rcVersion} = headerLenLength + fullHeaderLen v rcSupportKEM + C.authTagSize + paddedMsgLen +fullMsgLen Ratchet {rcSupportKEM} = headerLenLength + fullHeaderLen rcSupportKEM + C.authTagSize + paddedMsgLen where - v = current rcVersion - headerLenLength - | v >= pqRatchetE2EEncryptVersion = 3 -- two bytes are added because of two Large used in new encoding - | otherwise = 1 + headerLenLength = 3 -- two bytes are added because of two Large used in new encoding testMessageHeader :: forall a. AlgorithmI a => VersionE2E -> C.SAlgorithm a -> Expectation testMessageHeader v _ = do (k, _) <- atomically . C.generateKeyPair @a =<< C.newRandom let hdr = MsgHeader {msgMaxVersion = v, msgDHRs = k, msgKEM = Nothing, msgPN = 0, msgNs = 0} - parseAll (msgHeaderP v) (encodeMsgHeader v hdr) `shouldBe` Right hdr + smpDecode (smpEncode hdr) `shouldBe` Right hdr testKEMParams :: Expectation testKEMParams = do @@ -119,15 +115,15 @@ testMessageHeaderKEM _ = do g <- C.newRandom (k, _) <- atomically $ C.generateKeyPair @a g (kem, _) <- sntrup761Keypair g - let msgMaxVersion = max pqRatchetE2EEncryptVersion currentE2EEncryptVersion + let msgMaxVersion = currentE2EEncryptVersion msgKEM = Just . ARKP SRKSProposed $ RKParamsProposed kem hdr = MsgHeader {msgMaxVersion, msgDHRs = k, msgKEM, msgPN = 0, msgNs = 0} - parseAll (msgHeaderP msgMaxVersion) (encodeMsgHeader msgMaxVersion hdr) `shouldBe` Right hdr + smpDecode (smpEncode hdr) `shouldBe` Right hdr (kem', _) <- sntrup761Keypair g (ct, _) <- sntrup761Enc g kem let msgKEM' = Just . ARKP SRKSAccepted $ RKParamsAccepted ct kem' hdr' = MsgHeader {msgMaxVersion, msgDHRs = k, msgKEM = msgKEM', msgPN = 0, msgNs = 0} - parseAll (msgHeaderP msgMaxVersion) (encodeMsgHeader msgMaxVersion hdr') `shouldBe` Right hdr' + smpDecode (smpEncode hdr') `shouldBe` Right hdr' pattern Decrypted :: ByteString -> Either CryptoError (Either CryptoError ByteString) pattern Decrypted msg <- Right (Right msg) @@ -380,7 +376,7 @@ testEncodeDecode x = do testX3dh :: forall a. (AlgorithmI a, DhAlgorithm a) => C.SAlgorithm a -> IO () testX3dh _ = do g <- C.newRandom - let v = max pqRatchetE2EEncryptVersion currentE2EEncryptVersion + let v = currentE2EEncryptVersion (pksBob@(_, _, Nothing), AE2ERatchetParams _ e2eBob) <- liftIO $ generateSndE2EParams @a g v Nothing (pksAlice@(_, _, Nothing), e2eAlice) <- liftIO $ generateRcvE2EParams @a g v PQSupportOff let paramsBob = pqX3dhSnd pksBob e2eAlice @@ -399,7 +395,7 @@ testX3dhV1 _ = do testPqX3dhProposeInReply :: forall a. (AlgorithmI a, DhAlgorithm a) => C.SAlgorithm a -> IO () testPqX3dhProposeInReply _ = do g <- C.newRandom - let v = max pqRatchetE2EEncryptVersion currentE2EEncryptVersion + let v = currentE2EEncryptVersion -- initiate (no KEM) (pksAlice@(_, _, Nothing), e2eAlice) <- liftIO $ generateRcvE2EParams @a g v PQSupportOff -- propose KEM in reply @@ -411,7 +407,7 @@ testPqX3dhProposeInReply _ = do testPqX3dhProposeAccept :: forall a. (AlgorithmI a, DhAlgorithm a) => C.SAlgorithm a -> IO () testPqX3dhProposeAccept _ = do g <- C.newRandom - let v = max pqRatchetE2EEncryptVersion currentE2EEncryptVersion + let v = currentE2EEncryptVersion -- initiate (propose KEM) (pksAlice@(_, _, Just _), e2eAlice) <- liftIO $ generateRcvE2EParams @a g v PQSupportOn E2ERatchetParams _ _ _ (Just (RKParamsProposed aliceKem)) <- pure e2eAlice @@ -424,7 +420,7 @@ testPqX3dhProposeAccept _ = do testPqX3dhProposeReject :: forall a. (AlgorithmI a, DhAlgorithm a) => C.SAlgorithm a -> IO () testPqX3dhProposeReject _ = do g <- C.newRandom - let v = max pqRatchetE2EEncryptVersion currentE2EEncryptVersion + let v = currentE2EEncryptVersion -- initiate (propose KEM) (pksAlice@(_, _, Just _), e2eAlice) <- liftIO $ generateRcvE2EParams @a g v PQSupportOn E2ERatchetParams _ _ _ (Just (RKParamsProposed _)) <- pure e2eAlice @@ -437,7 +433,7 @@ testPqX3dhProposeReject _ = do testPqX3dhAcceptWithoutProposalError :: forall a. (AlgorithmI a, DhAlgorithm a) => C.SAlgorithm a -> IO () testPqX3dhAcceptWithoutProposalError _ = do g <- C.newRandom - let v = max pqRatchetE2EEncryptVersion currentE2EEncryptVersion + let v = currentE2EEncryptVersion -- initiate (no KEM) (pksAlice@(_, _, Nothing), e2eAlice) <- liftIO $ generateRcvE2EParams @a g v PQSupportOff E2ERatchetParams _ _ _ Nothing <- pure e2eAlice @@ -451,7 +447,7 @@ testPqX3dhAcceptWithoutProposalError _ = do testPqX3dhProposeAgain :: forall a. (AlgorithmI a, DhAlgorithm a) => C.SAlgorithm a -> IO () testPqX3dhProposeAgain _ = do g <- C.newRandom - let v = max pqRatchetE2EEncryptVersion currentE2EEncryptVersion + let v = currentE2EEncryptVersion -- initiate (propose KEM) (pksAlice@(_, _, Just _), e2eAlice) <- liftIO $ generateRcvE2EParams @a g v PQSupportOn E2ERatchetParams _ _ _ (Just (RKParamsProposed _)) <- pure e2eAlice @@ -514,7 +510,7 @@ withRatchets_ initRatchets_ test = do initRatchets :: (AlgorithmI a, DhAlgorithm a) => IO (Ratchet a, Ratchet a, Encrypt a, Decrypt a, EncryptDecryptSpec a) initRatchets = do g <- C.newRandom - let v = max pqRatchetE2EEncryptVersion currentE2EEncryptVersion + let v = currentE2EEncryptVersion (pksBob@(_, _, Nothing), AE2ERatchetParams _ e2eBob) <- liftIO $ generateSndE2EParams g v Nothing (pksAlice@(_, pkAlice2, Nothing), e2eAlice) <- liftIO $ generateRcvE2EParams g v PQSupportOff Right paramsBob <- pure $ pqX3dhSnd pksBob e2eAlice @@ -528,7 +524,7 @@ initRatchets = do initRatchetsKEMProposed :: forall a. (AlgorithmI a, DhAlgorithm a) => IO (Ratchet a, Ratchet a, Encrypt a, Decrypt a, EncryptDecryptSpec a) initRatchetsKEMProposed = do g <- C.newRandom - let v = max pqRatchetE2EEncryptVersion currentE2EEncryptVersion + let v = currentE2EEncryptVersion -- initiate (no KEM) (pksAlice@(_, pkAlice2, Nothing), e2eAlice) <- liftIO $ generateRcvE2EParams g v PQSupportOff -- propose KEM in reply @@ -545,7 +541,7 @@ initRatchetsKEMProposed = do initRatchetsKEMAccepted :: forall a. (AlgorithmI a, DhAlgorithm a) => IO (Ratchet a, Ratchet a, Encrypt a, Decrypt a, EncryptDecryptSpec a) initRatchetsKEMAccepted = do g <- C.newRandom - let v = max pqRatchetE2EEncryptVersion currentE2EEncryptVersion + let v = currentE2EEncryptVersion -- initiate (propose) (pksAlice@(_, pkAlice2, Just _), e2eAlice) <- liftIO $ generateRcvE2EParams g v PQSupportOn E2ERatchetParams _ _ _ (Just (RKParamsProposed aliceKem)) <- pure e2eAlice @@ -563,7 +559,7 @@ initRatchetsKEMAccepted = do initRatchetsKEMProposedAgain :: forall a. (AlgorithmI a, DhAlgorithm a) => IO (Ratchet a, Ratchet a, Encrypt a, Decrypt a, EncryptDecryptSpec a) initRatchetsKEMProposedAgain = do g <- C.newRandom - let v = max pqRatchetE2EEncryptVersion currentE2EEncryptVersion + let v = currentE2EEncryptVersion -- initiate (propose KEM) (pksAlice@(_, pkAlice2, Just _), e2eAlice) <- liftIO $ generateRcvE2EParams g v PQSupportOn -- propose KEM again in reply diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index b3c3f2cf9..d831cdea7 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -30,6 +30,7 @@ module AgentTests.FunctionalAPITests makeConnection, exchangeGreetings, switchComplete, + fastSwitchComplete, createConnection, joinConnection, sendMessage, @@ -52,6 +53,7 @@ module AgentTests.FunctionalAPITests pattern Msg', pattern SENT, agentCfgVPrevPQ, + agentCfgV7, ) where @@ -233,8 +235,9 @@ smpCfgVPrev = (smpCfg agentCfg) {serverVRange = prevRange $ serverVRange $ smpCf -- ntfCfgVPrev :: ProtocolClientConfig NTFVersion -- ntfCfgVPrev = (ntfCfg agentCfg) {clientALPN = Nothing, serverVRange = V.mkVersionRange (VersionNTF 1) (VersionNTF 1)} +-- currently, previous e2e version is not supported agentCfgVPrev :: AgentConfig -agentCfgVPrev = agentCfgVPrevPQ {e2eEncryptVRange = prevRange $ e2eEncryptVRange agentCfg} +agentCfgVPrev = agentCfgVPrevPQ -- {e2eEncryptVRange = prevRange $ e2eEncryptVRange agentCfg} agentCfgVPrevPQ :: AgentConfig agentCfgVPrevPQ = @@ -336,13 +339,13 @@ functionalAPITests ps = do describe "two way concurrently (50)" $ testMatrix2Stress ps $ runAgentClientStressTestConc 50 xdescribe "two way concurrently (1000)" $ testMatrix2Stress ps $ runAgentClientStressTestConc 1000 describe "Establishing duplex connection, different PQ settings" $ do - testPQMatrix2 ps $ runAgentClientTestPQ True True + testPQMatrix2 ps $ runAgentClientTestPQ True describe "Establishing duplex connection v2, different Ratchet versions" $ testRatchetMatrix2 ps runAgentClientTest describe "Establish duplex connection via contact address" $ testMatrix2 ps runAgentClientContactTest describe "Establish duplex connection via contact address, different PQ settings" $ do - testPQMatrix2NoInv ps $ runAgentClientContactTestPQ True True PQSupportOn + testPQMatrix2NoInv ps $ runAgentClientContactTestPQ True True describe "Establish duplex connection via contact address v2, different Ratchet versions" $ testRatchetMatrix2 ps runAgentClientContactTest describe "Establish duplex connection via contact address, different PQ settings (3 clients)" $ do @@ -503,7 +506,7 @@ functionalAPITests ps = do testWaitDeliveryNoPending ps it "should delete connection after waiting for delivery to complete" $ testWaitDelivery ps - it "should delete connection if message can'ps be delivered due to AUTH error" $ + it "should delete connection if message can't be delivered due to AUTH error" $ testWaitDeliveryAUTHErr ps it "should delete connection by timeout even if message wasn't delivered" $ testWaitDeliveryTimeout ps @@ -526,6 +529,10 @@ functionalAPITests ps = do it "should handle service unavailable on startup" $ testServiceUnavailableOnStartup ps it "migrate connections to and from service" $ testMigrateConnectionsToService ps describe "Connection switch" $ do + describe "should switch delivery to the new queue with fast rotation" $ + testServerMatrix2 ps testFastSwitchConnection + it "should switch delivery to the new queue when the old server is down" $ + testFastSwitchDeadOldServer ps describe "should switch delivery to the new queue" $ testServerMatrix2 ps testSwitchConnection describe "should switch to new queue asynchronously" $ @@ -578,7 +585,6 @@ functionalAPITests ps = do withSmpServer ps testRatchetAdHash describe "Delivery receipts" $ do it "should send and receive delivery receipt" $ withSmpServer ps testDeliveryReceipts - it "should send delivery receipt only in connection v3+" $ testDeliveryReceiptsVersion ps it "send delivery receipts concurrently with messages" $ testDeliveryReceiptsConcurrent ps describe "user network info" $ do it "should wait for user network" testWaitForUserNetwork @@ -613,27 +619,27 @@ testMatrix2 :: HasCallStack => (ASrvTransport, AStoreType) -> (PQSupport -> SndQ testMatrix2 ps runTest = do it "current, via proxy" $ withSmpServerProxy ps $ runTestCfgServers2 agentCfg agentCfg initAgentServersProxy 1 $ runTest PQSupportOn True True it "current" $ withSmpServer ps $ runTestCfg2 agentCfg agentCfg 1 $ runTest PQSupportOn True False - it "prev" $ withSmpServer ps $ runTestCfg2 agentCfgVPrev agentCfgVPrev 1 $ runTest PQSupportOff False False - it "prev to current" $ withSmpServer ps $ runTestCfg2 agentCfgVPrev agentCfg 1 $ runTest PQSupportOff False False - it "current to prev" $ withSmpServer ps $ runTestCfg2 agentCfg agentCfgVPrev 1 $ runTest PQSupportOff False False + it "prev" $ withSmpServer ps $ runTestCfg2 agentCfgVPrev agentCfgVPrev 1 $ runTest PQSupportOff True False + it "prev to current" $ withSmpServer ps $ runTestCfg2 agentCfgVPrev agentCfg 1 $ runTest PQSupportOff True False + it "current to prev" $ withSmpServer ps $ runTestCfg2 agentCfg agentCfgVPrev 1 $ runTest PQSupportOff True False -testMatrix2Stress :: HasCallStack => (ASrvTransport, AStoreType) -> (PQSupport -> SndQueueSecured -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec +testMatrix2Stress :: HasCallStack => (ASrvTransport, AStoreType) -> (PQSupport -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec testMatrix2Stress ps runTest = do - it "current, via proxy" $ withSmpServerProxy ps $ runTestCfgServers2 aCfg aCfg initAgentServersProxy 1 $ runTest PQSupportOn True True - it "current" $ withSmpServer ps $ runTestCfg2 aCfg aCfg 1 $ runTest PQSupportOn True False - it "prev" $ withSmpServer ps $ runTestCfg2 aCfgVPrev aCfgVPrev 1 $ runTest PQSupportOff False False - it "prev to current" $ withSmpServer ps $ runTestCfg2 aCfgVPrev aCfg 1 $ runTest PQSupportOff False False - it "current to prev" $ withSmpServer ps $ runTestCfg2 aCfg aCfgVPrev 1 $ runTest PQSupportOff False False + it "current, via proxy" $ withSmpServerProxy ps $ runTestCfgServers2 aCfg aCfg initAgentServersProxy 1 $ runTest PQSupportOn True + it "current" $ withSmpServer ps $ runTestCfg2 aCfg aCfg 1 $ runTest PQSupportOn False + it "prev" $ withSmpServer ps $ runTestCfg2 aCfgVPrev aCfgVPrev 1 $ runTest PQSupportOff False + it "prev to current" $ withSmpServer ps $ runTestCfg2 aCfgVPrev aCfg 1 $ runTest PQSupportOff False + it "current to prev" $ withSmpServer ps $ runTestCfg2 aCfg aCfgVPrev 1 $ runTest PQSupportOff False where aCfg = agentCfg {messageRetryInterval = fastMessageRetryInterval} aCfgVPrev = agentCfgVPrev {messageRetryInterval = fastMessageRetryInterval} -testBasicMatrix2 :: HasCallStack => (ASrvTransport, AStoreType) -> (SndQueueSecured -> AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec +testBasicMatrix2 :: HasCallStack => (ASrvTransport, AStoreType) -> (AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec testBasicMatrix2 ps runTest = do - it "current" $ withSmpServer ps $ runTestCfg2 agentCfg agentCfg 1 $ runTest True - it "prev" $ withSmpServer ps $ runTestCfg2 agentCfgVPrevPQ agentCfgVPrevPQ 1 $ runTest False - it "prev to current" $ withSmpServer ps $ runTestCfg2 agentCfgVPrevPQ agentCfg 1 $ runTest False - it "current to prev" $ withSmpServer ps $ runTestCfg2 agentCfg agentCfgVPrevPQ 1 $ runTest False + it "current" $ withSmpServer ps $ runTestCfg2 agentCfg agentCfg 1 runTest + it "prev" $ withSmpServer ps $ runTestCfg2 agentCfgVPrevPQ agentCfgVPrevPQ 1 runTest + it "prev to current" $ withSmpServer ps $ runTestCfg2 agentCfgVPrevPQ agentCfg 1 runTest + it "current to prev" $ withSmpServer ps $ runTestCfg2 agentCfg agentCfgVPrevPQ 1 runTest testRatchetMatrix2 :: HasCallStack => (ASrvTransport, AStoreType) -> (PQSupport -> SndQueueSecured -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec testRatchetMatrix2 ps runTest = do @@ -741,16 +747,16 @@ withAgentClients3 runTest = runTest a b c runAgentClientTest :: HasCallStack => PQSupport -> SndQueueSecured -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO () -runAgentClientTest pqSupport sqSecured viaProxy alice bob baseId = - runAgentClientTestPQ sqSecured viaProxy (alice, IKLinkPQ pqSupport) (bob, pqSupport) baseId +runAgentClientTest pqSupport _sqSecured viaProxy alice bob baseId = + runAgentClientTestPQ viaProxy (alice, IKLinkPQ pqSupport) (bob, pqSupport) baseId -runAgentClientTestPQ :: HasCallStack => SndQueueSecured -> Bool -> (AgentClient, InitialKeys) -> (AgentClient, PQSupport) -> AgentMsgId -> IO () -runAgentClientTestPQ sqSecured viaProxy (alice, aPQ) (bob, bPQ) baseId = +runAgentClientTestPQ :: HasCallStack => Bool -> (AgentClient, InitialKeys) -> (AgentClient, PQSupport) -> AgentMsgId -> IO () +runAgentClientTestPQ viaProxy (alice, aPQ) (bob, bPQ) baseId = runRight_ $ do (bobId, CCLink qInfo Nothing) <- A.createConnection alice NRMInteractive 1 True True SCMInvitation Nothing Nothing aPQ False SMSubscribe aliceId <- A.prepareConnectionToJoin bob 1 True qInfo bPQ sqSecured' <- A.joinConnection bob NRMInteractive 1 aliceId True qInfo "bob's connInfo" bPQ SMSubscribe - liftIO $ sqSecured' `shouldBe` sqSecured + liftIO $ sqSecured' `shouldBe` True ("", _, A.CONF confId pqSup' _ "bob's connInfo") <- get alice liftIO $ pqSup' `shouldBe` CR.connPQEncryption aPQ allowConnection alice bobId confId "alice's connInfo" @@ -787,10 +793,10 @@ runAgentClientTestPQ sqSecured viaProxy (alice, aPQ) (bob, bPQ) baseId = pqConnectionMode :: InitialKeys -> PQSupport -> Bool pqConnectionMode pqMode1 pqMode2 = supportPQ (CR.connPQEncryption pqMode1) && supportPQ pqMode2 -runAgentClientStressTestOneWay :: HasCallStack => Int64 -> PQSupport -> SndQueueSecured -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO () -runAgentClientStressTestOneWay n pqSupport sqSecured viaProxy alice bob baseId = runRight_ $ do +runAgentClientStressTestOneWay :: HasCallStack => Int64 -> PQSupport -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO () +runAgentClientStressTestOneWay n pqSupport viaProxy alice bob baseId = runRight_ $ do let pqEnc = PQEncryption $ supportPQ pqSupport - (aliceId, bobId) <- makeConnection_ pqSupport sqSecured alice bob + (aliceId, bobId) <- makeConnection_ pqSupport alice bob let proxySrv = if viaProxy then Just testSMPServer else Nothing message i = "message " <> bshow i concurrently_ @@ -819,9 +825,9 @@ runAgentClientStressTestOneWay n pqSupport sqSecured viaProxy alice bob baseId = where msgId = subtract baseId . fst -runAgentClientStressTestConc :: HasCallStack => Int64 -> PQSupport -> SndQueueSecured -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO () -runAgentClientStressTestConc n pqSupport sqSecured viaProxy alice bob _baseId = runRight_ $ do - (aliceId, bobId) <- makeConnection_ pqSupport sqSecured alice bob +runAgentClientStressTestConc :: HasCallStack => Int64 -> PQSupport -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO () +runAgentClientStressTestConc n pqSupport viaProxy alice bob _baseId = runRight_ $ do + (aliceId, bobId) <- makeConnection_ pqSupport alice bob amId <- newTVarIO 0 bmId <- newTVarIO 0 let n2 = n `div` 2 @@ -883,13 +889,13 @@ testEnablePQEncryption :: HasCallStack => IO () testEnablePQEncryption = withAgentClients2 $ \ca cb -> runRight_ $ do g <- liftIO C.newRandom - (aId, bId) <- makeConnection_ PQSupportOff True ca cb + (aId, bId) <- makeConnection_ PQSupportOff ca cb let a = (ca, aId) b = (cb, bId) (a, 2, "msg 1") \#>\ b (b, 3, "msg 2") \#>\ a -- 45 bytes is used by agent message envelope inside double ratchet message envelope - let largeMsg g' pqEnc = atomically $ C.randomBytes (e2eEncAgentMsgLength pqdrSMPAgentVersion pqEnc - 45) g' + let largeMsg g' pqEnc = atomically $ C.randomBytes (e2eEncAgentMsgLength pqEnc - 45) g' lrg <- largeMsg g PQSupportOff (a, 4, lrg) \#>\ b (b, 5, lrg) \#>\ a @@ -971,17 +977,17 @@ testAgentClient3 = runAgentClientContactTest :: HasCallStack => PQSupport -> SndQueueSecured -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO () runAgentClientContactTest pqSupport sqSecured viaProxy alice bob baseId = - runAgentClientContactTestPQ sqSecured viaProxy pqSupport (alice, IKLinkPQ pqSupport) (bob, pqSupport) baseId + runAgentClientContactTestPQ sqSecured viaProxy (alice, IKLinkPQ pqSupport) (bob, pqSupport) baseId -runAgentClientContactTestPQ :: HasCallStack => SndQueueSecured -> Bool -> PQSupport -> (AgentClient, InitialKeys) -> (AgentClient, PQSupport) -> AgentMsgId -> IO () -runAgentClientContactTestPQ sqSecured viaProxy reqPQSupport (alice, aPQ) (bob, bPQ) baseId = +runAgentClientContactTestPQ :: HasCallStack => SndQueueSecured -> Bool -> (AgentClient, InitialKeys) -> (AgentClient, PQSupport) -> AgentMsgId -> IO () +runAgentClientContactTestPQ sqSecured viaProxy (alice, aPQ) (bob, bPQ) baseId = runRight_ $ do (_, CCLink qInfo Nothing) <- A.createConnection alice NRMInteractive 1 True True SCMContact Nothing Nothing aPQ False SMSubscribe aliceId <- A.prepareConnectionToJoin bob 1 True qInfo bPQ sqSecuredJoin <- A.joinConnection bob NRMInteractive 1 aliceId True qInfo "bob's connInfo" bPQ SMSubscribe liftIO $ sqSecuredJoin `shouldBe` False -- joining via contact address connection ("", _, A.REQ invId pqSup' _ "bob's connInfo" _) <- get alice - liftIO $ pqSup' `shouldBe` reqPQSupport + liftIO $ pqSup' `shouldBe` PQSupportOn bobId <- A.prepareConnectionToAccept alice 1 True invId (CR.connPQEncryption aPQ) sqSecured' <- acceptContact alice 1 bobId True invId "alice's connInfo" (CR.connPQEncryption aPQ) SMSubscribe liftIO $ sqSecured' `shouldBe` sqSecured @@ -2056,86 +2062,86 @@ connReqWithKeys cr rk = case cr of testIncreaseConnAgentVersion :: HasCallStack => (ASrvTransport, AStoreType) -> IO () testIncreaseConnAgentVersion ps = do - alice <- getSMPAgentClient' 1 agentCfg {smpAgentVRange = mkVersionRange 1 2} initAgentServers testDB - bob <- getSMPAgentClient' 2 agentCfg {smpAgentVRange = mkVersionRange 1 2} initAgentServers testDB2 + alice <- getSMPAgentClient' 1 agentCfg {smpAgentVRange = mkVersionRange 6 7} initAgentServers testDB + bob <- getSMPAgentClient' 2 agentCfg {smpAgentVRange = mkVersionRange 6 7} initAgentServers testDB2 withSmpServerStoreMsgLogOn ps testPort $ \_ -> do (aliceId, bobId) <- runRight $ do - (aliceId, bobId) <- makeConnection_ PQSupportOff False alice bob + (aliceId, bobId) <- makeConnection_ PQSupportOff alice bob exchangeGreetingsMsgId_ PQEncOff 2 alice bobId bob aliceId - checkVersion alice bobId 2 - checkVersion bob aliceId 2 + checkVersion alice bobId 7 + checkVersion bob aliceId 7 pure (aliceId, bobId) -- version doesn't increase if incompatible disposeAgentClient alice threadDelay 250000 - alice2 <- getSMPAgentClient' 3 agentCfg {smpAgentVRange = mkVersionRange 1 3} initAgentServers testDB + alice2 <- getSMPAgentClient' 3 agentCfg {smpAgentVRange = mkVersionRange 6 8} initAgentServers testDB runRight_ $ do subscribeConnection alice2 bobId exchangeGreetingsMsgId_ PQEncOff 4 alice2 bobId bob aliceId - checkVersion alice2 bobId 2 - checkVersion bob aliceId 2 + checkVersion alice2 bobId 7 + checkVersion bob aliceId 7 -- version increases if compatible disposeAgentClient bob threadDelay 250000 - bob2 <- getSMPAgentClient' 4 agentCfg {smpAgentVRange = mkVersionRange 1 3} initAgentServers testDB2 + bob2 <- getSMPAgentClient' 4 agentCfg {smpAgentVRange = mkVersionRange 6 8} initAgentServers testDB2 runRight_ $ do subscribeConnection bob2 aliceId exchangeGreetingsMsgId_ PQEncOff 6 alice2 bobId bob2 aliceId - checkVersion alice2 bobId 3 - checkVersion bob2 aliceId 3 + checkVersion alice2 bobId 8 + checkVersion bob2 aliceId 8 -- version doesn't decrease, even if incompatible disposeAgentClient alice2 threadDelay 250000 - alice3 <- getSMPAgentClient' 5 agentCfg {smpAgentVRange = mkVersionRange 2 2} initAgentServers testDB + alice3 <- getSMPAgentClient' 5 agentCfg {smpAgentVRange = mkVersionRange 7 7} initAgentServers testDB runRight_ $ do subscribeConnection alice3 bobId exchangeGreetingsMsgId_ PQEncOff 8 alice3 bobId bob2 aliceId - checkVersion alice3 bobId 3 - checkVersion bob2 aliceId 3 + checkVersion alice3 bobId 8 + checkVersion bob2 aliceId 8 disposeAgentClient bob2 threadDelay 250000 - bob3 <- getSMPAgentClient' 6 agentCfg {smpAgentVRange = mkVersionRange 1 1} initAgentServers testDB2 + bob3 <- getSMPAgentClient' 6 agentCfg {smpAgentVRange = mkVersionRange 6 6} initAgentServers testDB2 runRight_ $ do subscribeConnection bob3 aliceId exchangeGreetingsMsgId_ PQEncOff 10 alice3 bobId bob3 aliceId - checkVersion alice3 bobId 3 - checkVersion bob3 aliceId 3 + checkVersion alice3 bobId 8 + checkVersion bob3 aliceId 8 disposeAgentClient alice3 disposeAgentClient bob3 -checkVersion :: AgentClient -> ConnId -> Word16 -> ExceptT AgentErrorType IO () +checkVersion :: HasCallStack => AgentClient -> ConnId -> Word16 -> ExceptT AgentErrorType IO () checkVersion c connId v = do ConnectionStats {connAgentVersion} <- getConnectionServers c connId liftIO $ connAgentVersion `shouldBe` VersionSMPA v testIncreaseConnAgentVersionMaxCompatible :: HasCallStack => (ASrvTransport, AStoreType) -> IO () testIncreaseConnAgentVersionMaxCompatible ps = do - alice <- getSMPAgentClient' 1 agentCfg {smpAgentVRange = mkVersionRange 1 2} initAgentServers testDB - bob <- getSMPAgentClient' 2 agentCfg {smpAgentVRange = mkVersionRange 1 2} initAgentServers testDB2 + alice <- getSMPAgentClient' 1 agentCfg {smpAgentVRange = mkVersionRange 6 7} initAgentServers testDB + bob <- getSMPAgentClient' 2 agentCfg {smpAgentVRange = mkVersionRange 6 7} initAgentServers testDB2 withSmpServerStoreMsgLogOn ps testPort $ \_ -> do (aliceId, bobId) <- runRight $ do - (aliceId, bobId) <- makeConnection_ PQSupportOff False alice bob + (aliceId, bobId) <- makeConnection_ PQSupportOff alice bob exchangeGreetingsMsgId_ PQEncOff 2 alice bobId bob aliceId - checkVersion alice bobId 2 - checkVersion bob aliceId 2 + checkVersion alice bobId 7 + checkVersion bob aliceId 7 pure (aliceId, bobId) -- version increases to max compatible disposeAgentClient alice threadDelay 250000 - alice2 <- getSMPAgentClient' 3 agentCfg {smpAgentVRange = mkVersionRange 1 3} initAgentServers testDB + alice2 <- getSMPAgentClient' 3 agentCfg {smpAgentVRange = mkVersionRange 6 8} initAgentServers testDB disposeAgentClient bob threadDelay 250000 bob2 <- getSMPAgentClient' 4 agentCfg {smpAgentVRange = supportedSMPAgentVRange} initAgentServers testDB2 @@ -2144,34 +2150,34 @@ testIncreaseConnAgentVersionMaxCompatible ps = do subscribeConnection alice2 bobId subscribeConnection bob2 aliceId exchangeGreetingsMsgId_ PQEncOff 4 alice2 bobId bob2 aliceId - checkVersion alice2 bobId 3 - checkVersion bob2 aliceId 3 + checkVersion alice2 bobId 8 + checkVersion bob2 aliceId 8 disposeAgentClient alice2 disposeAgentClient bob2 testIncreaseConnAgentVersionStartDifferentVersion :: HasCallStack => (ASrvTransport, AStoreType) -> IO () testIncreaseConnAgentVersionStartDifferentVersion ps = do - alice <- getSMPAgentClient' 1 agentCfg {smpAgentVRange = mkVersionRange 1 2} initAgentServers testDB - bob <- getSMPAgentClient' 2 agentCfg {smpAgentVRange = mkVersionRange 1 3} initAgentServers testDB2 + alice <- getSMPAgentClient' 1 agentCfg {smpAgentVRange = mkVersionRange 6 7} initAgentServers testDB + bob <- getSMPAgentClient' 2 agentCfg {smpAgentVRange = mkVersionRange 6 8} initAgentServers testDB2 withSmpServerStoreMsgLogOn ps testPort $ \_ -> do (aliceId, bobId) <- runRight $ do - (aliceId, bobId) <- makeConnection_ PQSupportOff False alice bob + (aliceId, bobId) <- makeConnection_ PQSupportOff alice bob exchangeGreetingsMsgId_ PQEncOff 2 alice bobId bob aliceId - checkVersion alice bobId 2 - checkVersion bob aliceId 2 + checkVersion alice bobId 7 + checkVersion bob aliceId 7 pure (aliceId, bobId) -- version increases to max compatible disposeAgentClient alice threadDelay 250000 - alice2 <- getSMPAgentClient' 3 agentCfg {smpAgentVRange = mkVersionRange 1 3} initAgentServers testDB + alice2 <- getSMPAgentClient' 3 agentCfg {smpAgentVRange = mkVersionRange 6 8} initAgentServers testDB runRight_ $ do subscribeConnection alice2 bobId exchangeGreetingsMsgId_ PQEncOff 4 alice2 bobId bob aliceId - checkVersion alice2 bobId 3 - checkVersion bob aliceId 3 + checkVersion alice2 bobId 8 + checkVersion bob aliceId 8 disposeAgentClient alice2 disposeAgentClient bob @@ -2731,20 +2737,20 @@ testOnlyCreatePull = withAgentClients2 $ \alice bob -> runRight_ $ do getMSGNTF alice bobId makeConnection :: AgentClient -> AgentClient -> ExceptT AgentErrorType IO (ConnId, ConnId) -makeConnection = makeConnection_ PQSupportOn True +makeConnection = makeConnection_ PQSupportOn -makeConnection_ :: PQSupport -> SndQueueSecured -> AgentClient -> AgentClient -> ExceptT AgentErrorType IO (ConnId, ConnId) -makeConnection_ pqEnc sqSecured alice bob = makeConnectionForUsers_ pqEnc sqSecured alice 1 bob 1 +makeConnection_ :: PQSupport -> AgentClient -> AgentClient -> ExceptT AgentErrorType IO (ConnId, ConnId) +makeConnection_ pqEnc alice bob = makeConnectionForUsers_ pqEnc alice 1 bob 1 makeConnectionForUsers :: HasCallStack => AgentClient -> UserId -> AgentClient -> UserId -> ExceptT AgentErrorType IO (ConnId, ConnId) -makeConnectionForUsers = makeConnectionForUsers_ PQSupportOn True +makeConnectionForUsers = makeConnectionForUsers_ PQSupportOn -makeConnectionForUsers_ :: HasCallStack => PQSupport -> SndQueueSecured -> AgentClient -> UserId -> AgentClient -> UserId -> ExceptT AgentErrorType IO (ConnId, ConnId) -makeConnectionForUsers_ pqSupport sqSecured alice aliceUserId bob bobUserId = do +makeConnectionForUsers_ :: HasCallStack => PQSupport -> AgentClient -> UserId -> AgentClient -> UserId -> ExceptT AgentErrorType IO (ConnId, ConnId) +makeConnectionForUsers_ pqSupport alice aliceUserId bob bobUserId = do (bobId, CCLink qInfo Nothing) <- A.createConnection alice NRMInteractive aliceUserId True True SCMInvitation Nothing Nothing (IKLinkPQ pqSupport) False SMSubscribe aliceId <- A.prepareConnectionToJoin bob bobUserId True qInfo pqSupport sqSecured' <- A.joinConnection bob NRMInteractive bobUserId aliceId True qInfo "bob's connInfo" pqSupport SMSubscribe - liftIO $ sqSecured' `shouldBe` sqSecured + liftIO $ sqSecured' `shouldBe` True ("", _, A.CONF confId pqSup' _ "bob's connInfo") <- get alice liftIO $ pqSup' `shouldBe` pqSupport allowConnection alice bobId confId "alice's connInfo" @@ -2872,7 +2878,7 @@ testBatchedSubscriptions :: Int -> Int -> (ASrvTransport, AStoreType) -> IO () testBatchedSubscriptions nCreate nDel ps@(t, ASType qsType _) = do (conns, conns') <- withAgentClientsCfgServers2 agentCfg agentCfg initAgentServers2 $ \a b -> do conns <- runServers $ do - conns <- replicateM nCreate $ makeConnection_ PQSupportOff True a b + conns <- replicateM nCreate $ makeConnection_ PQSupportOff a b forM_ conns $ \(aId, bId) -> exchangeGreetings_ PQEncOff a bId b aId let (aIds', bIds') = unzip $ take nDel conns delete a bIds' @@ -3017,8 +3023,8 @@ receiveMsg c cId msgId msg = do get c =##> \case ("", cId', Msg' mId' PQEncOn msg') -> cId' == cId && mId' == msgId && msg' == msg; _ -> False ackMessage c cId msgId Nothing -testAsyncCommands :: SndQueueSecured -> AgentClient -> AgentClient -> AgentMsgId -> IO () -testAsyncCommands sqSecured alice bob baseId = +testAsyncCommands :: AgentClient -> AgentClient -> AgentMsgId -> IO () +testAsyncCommands alice bob baseId = runRight_ $ do bobId <- prepareConnectionToCreate alice 1 True SCMInvitation PQSupportOn createConnectionAsync alice "1" bobId True SCMInvitation IKPQOn False SMSubscribe @@ -3029,7 +3035,7 @@ testAsyncCommands sqSecured alice bob baseId = ("2", aliceId', JOINED sqSecured') <- get bob liftIO $ do aliceId' `shouldBe` aliceId - sqSecured' `shouldBe` sqSecured + sqSecured' `shouldBe` True ("", _, CONF confId _ "bob's connInfo") <- get alice allowConnectionAsync alice "3" bobId confId "alice's connInfo" get alice =##> \case ("3", _, OK) -> True; _ -> False @@ -3145,8 +3151,8 @@ testAsyncCommandsRestore ps = do get alice' =##> \case ("1", _, INV _) -> True; _ -> False pure () -testAcceptContactAsync :: SndQueueSecured -> AgentClient -> AgentClient -> AgentMsgId -> IO () -testAcceptContactAsync sqSecured alice bob baseId = +testAcceptContactAsync :: AgentClient -> AgentClient -> AgentMsgId -> IO () +testAcceptContactAsync alice bob baseId = runRight_ $ do (_, qInfo) <- createConnection alice 1 True SCMContact Nothing SMSubscribe (aliceId, sqSecuredJoin) <- joinConnection bob 1 True qInfo "bob's connInfo" SMSubscribe @@ -3154,7 +3160,7 @@ testAcceptContactAsync sqSecured alice bob baseId = ("", _, REQ invId _ "bob's connInfo") <- get alice bobId <- prepareConnectionToAccept alice 1 True invId PQSupportOn acceptContactAsync alice "1" bobId True invId "alice's connInfo" PQSupportOn SMSubscribe - get alice =##> \case ("1", c, JOINED sqSecured') -> c == bobId && sqSecured' == sqSecured; _ -> False + get alice =##> \case ("1", c, JOINED sqSecured') -> c == bobId && sqSecured' == True; _ -> False ("", _, CONF confId _ "alice's connInfo") <- get bob allowConnection bob aliceId confId "bob's connInfo" get alice ##> ("", bobId, INFO "bob's connInfo") @@ -3517,9 +3523,13 @@ testUsersNoServer ps = withAgentClientsCfg2 aCfg agentCfg $ \a b -> do where aCfg = agentCfg {initialCleanupDelay = 10000, cleanupInterval = 10000, deleteErrorCount = 3} +-- fast rotation runs at agent version 8+; these tests pin to v7 to exercise the QKEY/QUSE slow path and switch abort +agentCfgV7 :: AgentConfig +agentCfgV7 = agentCfg {smpAgentVRange = mkVersionRange 6 7} + testSwitchConnection :: InitialAgentServers -> IO () testSwitchConnection servers = - withAgentClientsCfgServers2 agentCfg agentCfg servers $ \a b -> runRight_ $ do + withAgentClientsCfgServers2 agentCfgV7 agentCfgV7 servers $ \a b -> runRight_ $ do (aId, bId) <- makeConnection a b exchangeGreetings a bId b aId testFullSwitch a bId b aId 8 @@ -3543,6 +3553,68 @@ switchComplete a bId b aId = do phaseSnd b aId SPCompleted [Nothing] phaseRcv a bId SPCompleted [Nothing] +testFastSwitchConnection :: InitialAgentServers -> IO () +testFastSwitchConnection servers = + withAgentClientsCfgServers2 agentCfg agentCfg servers $ \a b -> runRight_ $ do + (aId, bId) <- makeConnection a b + exchangeGreetings a bId b aId + stats <- switchConnectionAsync a "" bId + liftIO $ rcvSwchStatuses' stats `shouldMatchList` [Just RSSwitchStarted] + fastSwitchComplete a bId b aId + exchangeGreetingsMsgId 6 a bId b aId + +fastSwitchComplete :: AgentClient -> ByteString -> AgentClient -> ByteString -> ExceptT AgentErrorType IO () +fastSwitchComplete a bId b aId = do + phaseRcv a bId SPStarted [Just RSSendingQADD, Nothing] + phaseSnd b aId SPStarted [Just SSSecuringQueue, Nothing] + phaseSnd b aId SPSecured [Just SSSendingQEND, Nothing] + phaseRcv a bId SPConfirmed [Just RSSendingQADD, Nothing] + phaseRcv a bId SPCompleted [Nothing] + phaseSnd b aId SPCompleted [Nothing] + +-- A's old receive queue is on server1 (stopped after the connection is set up); B's queue and the new queue are on server2. +-- Fast rotation completes over the live server: B secures the new queue and sends the confirmation and QEND on it, +-- so the recipient moves to it and removes the old queue without the old server. +testFastSwitchDeadOldServer :: HasCallStack => (ASrvTransport, AStoreType) -> IO () +testFastSwitchDeadOldServer ps@(t, ASType qsType _) = do + let bServers = initAgentServers {smp = userServers [testSMPServer2]} + withSmpServerConfigOn t (cfgJ2QS qsType) testPort2 $ \_ -> + withAgent 1 agentCfg initAgentServers testDB $ \a -> + withAgent 2 agentCfg bServers testDB2 $ \b -> do + (aId, bId) <- withSmpServerStoreLogOn ps testPort $ \_ -> runRight $ do + (aId, bId) <- makeConnection a b + exchangeGreetings a bId b aId + -- create the rotated queue on the live server + liftIO $ setProtocolServers a 1 [noAuthSrvCfg testSMPServer2] + pure (aId, bId) + nGet a =##> \case ("", "", DOWN _ cs) -> bId `elem` cs; _ -> False + runRight_ $ do + -- a message queued while the old server is down must survive the rotation and arrive on the new queue + _ <- sendMessage b aId SMP.noMsgFlags "queued while down" + _ <- switchConnectionAsync a "" bId + queuedReceived <- drainSwitchCompletedRcvMsg a bId "queued while down" + liftIO $ queuedReceived `shouldBe` True + drainSwitchCompleted b aId QDSnd + exchangeGreetingsMsgId 7 a bId b aId + +-- drains switch and network events until the connection reports SPCompleted in the given direction, +-- tolerating DOWN/UP and intermediate phases (the old server is stopped mid-rotation) +drainSwitchCompleted :: AgentClient -> ByteString -> QueueDirection -> ExceptT AgentErrorType IO () +drainSwitchCompleted c connId d = + pGet c >>= \case + (_, connId', AEvt SAEConn (SWITCH d' SPCompleted _)) | connId' == connId && d' == d -> pure () + _ -> drainSwitchCompleted c connId d + +-- like drainSwitchCompleted for QDRcv, additionally acking and reporting a message matching the body seen while draining +drainSwitchCompletedRcvMsg :: AgentClient -> ByteString -> MsgBody -> ExceptT AgentErrorType IO Bool +drainSwitchCompletedRcvMsg c connId body = go False + where + go seen = + pGet c >>= \case + (_, connId', AEvt SAEConn (SWITCH QDRcv SPCompleted _)) | connId' == connId -> pure seen + (_, connId', AEvt SAEConn (Msg' mId _ body')) | connId' == connId && body' == body -> ackMessage c connId' mId Nothing >> go True + _ -> go seen + phaseRcv :: AgentClient -> ByteString -> SwitchPhase -> [Maybe RcvSwitchStatus] -> ExceptT AgentErrorType IO () phaseRcv c connId p swchStatuses = phase c connId QDRcv p (\stats -> rcvSwchStatuses' stats `shouldMatchList` swchStatuses) @@ -3599,9 +3671,9 @@ testSwitchAsync servers = do testFullSwitch a bId b aId 14 where withA :: (AgentClient -> IO a) -> IO a - withA = withAgent 1 agentCfg servers testDB + withA = withAgent 1 agentCfgV7 servers testDB withB :: (AgentClient -> IO a) -> IO a - withB = withAgent 2 agentCfg servers testDB2 + withB = withAgent 2 agentCfgV7 servers testDB2 withAgent :: HasCallStack => Int -> AgentConfig -> InitialAgentServers -> String -> (HasCallStack => AgentClient -> IO a) -> IO a withAgent clientId cfg' servers dbPath = bracket (getSMPAgentClient' clientId cfg' servers dbPath) (\a -> disposeAgentClient a >> threadDelay 100000) @@ -3617,7 +3689,7 @@ sessionSubscribe withC connIds a = testSwitchDelete :: InitialAgentServers -> IO () testSwitchDelete servers = - withAgentClientsCfgServers2 agentCfg agentCfg servers $ \a b -> runRight_ $ do + withAgentClientsCfgServers2 agentCfgV7 agentCfgV7 servers $ \a b -> runRight_ $ do (aId, bId) <- makeConnection a b exchangeGreetings a bId b aId liftIO $ disposeAgentClient b @@ -3675,9 +3747,9 @@ testAbortSwitchStarted servers = do testFullSwitch a bId b aId 16 where withA :: (AgentClient -> IO a) -> IO a - withA = withAgent 1 agentCfg servers testDB + withA = withAgent 1 agentCfgV7 servers testDB withB :: (AgentClient -> IO a) -> IO a - withB = withAgent 2 agentCfg servers testDB2 + withB = withAgent 2 agentCfgV7 servers testDB2 testAbortSwitchStartedReinitiate :: HasCallStack => InitialAgentServers -> IO () testAbortSwitchStartedReinitiate servers = do @@ -3726,9 +3798,9 @@ testAbortSwitchStartedReinitiate servers = do testFullSwitch a bId b aId 16 where withA :: (AgentClient -> IO a) -> IO a - withA = withAgent 1 agentCfg servers testDB + withA = withAgent 1 agentCfgV7 servers testDB withB :: (AgentClient -> IO a) -> IO a - withB = withAgent 2 agentCfg servers testDB2 + withB = withAgent 2 agentCfgV7 servers testDB2 switchPhaseRcvP :: ConnId -> SwitchPhase -> [Maybe RcvSwitchStatus] -> ATransmission -> Bool switchPhaseRcvP cId sphase swchStatuses = switchPhaseP cId QDRcv sphase (\stats -> rcvSwchStatuses' stats == swchStatuses) @@ -3780,9 +3852,9 @@ testCannotAbortSwitchSecured servers = do testFullSwitch a bId b aId 14 where withA :: (AgentClient -> IO a) -> IO a - withA = withAgent 1 agentCfg servers testDB + withA = withAgent 1 agentCfgV7 servers testDB withB :: (AgentClient -> IO a) -> IO a - withB = withAgent 2 agentCfg servers testDB2 + withB = withAgent 2 agentCfgV7 servers testDB2 testSwitch2Connections :: HasCallStack => InitialAgentServers -> IO () testSwitch2Connections servers = do @@ -3838,9 +3910,9 @@ testSwitch2Connections servers = do testFullSwitch a bId2 b aId2 14 where withA :: (AgentClient -> IO a) -> IO a - withA = withAgent 1 agentCfg servers testDB + withA = withAgent 1 agentCfgV7 servers testDB withB :: (AgentClient -> IO a) -> IO a - withB = withAgent 2 agentCfg servers testDB2 + withB = withAgent 2 agentCfgV7 servers testDB2 testSwitch2ConnectionsAbort1 :: HasCallStack => InitialAgentServers -> IO () testSwitch2ConnectionsAbort1 servers = do @@ -3891,9 +3963,9 @@ testSwitch2ConnectionsAbort1 servers = do testFullSwitch a bId2 b aId2 12 where withA :: (AgentClient -> IO a) -> IO a - withA = withAgent 1 agentCfg servers testDB + withA = withAgent 1 agentCfgV7 servers testDB withB :: (AgentClient -> IO a) -> IO a - withB = withAgent 2 agentCfg servers testDB2 + withB = withAgent 2 agentCfgV7 servers testDB2 testCreateQueueAuth :: HasCallStack => (Maybe BasicAuth, VersionSMP) -> (Maybe BasicAuth, VersionSMP) -> SndQueueSecured -> AgentMsgId -> IO Int testCreateQueueAuth clnt1 clnt2 sqSecured baseId = do @@ -3978,59 +4050,6 @@ testDeliveryReceipts = ackMessage b aId 5 (Just "") `catchError` \case (A.CMD PROHIBITED _) -> pure (); e -> liftIO $ expectationFailure ("unexpected error " <> show e) ackMessage b aId 5 Nothing -testDeliveryReceiptsVersion :: HasCallStack => (ASrvTransport, AStoreType) -> IO () -testDeliveryReceiptsVersion ps = do - a <- getSMPAgentClient' 1 agentCfg {smpAgentVRange = mkVersionRange 1 3} initAgentServers testDB - b <- getSMPAgentClient' 2 agentCfg {smpAgentVRange = mkVersionRange 1 3} initAgentServers testDB2 - withSmpServerStoreMsgLogOn ps testPort $ \_ -> do - (aId, bId) <- runRight $ do - (aId, bId) <- makeConnection_ PQSupportOff False a b - checkVersion a bId 3 - checkVersion b aId 3 - (2, _) <- A.sendMessage a bId PQEncOff SMP.noMsgFlags "hello" - get a ##> ("", bId, SENT 2) - get b =##> \case ("", c, Msg' 2 PQEncOff "hello") -> c == aId; _ -> False - ackMessage b aId 2 $ Just "" - liftIO $ noMessages a "no delivery receipt (unsupported version)" - (3, _) <- A.sendMessage b aId PQEncOff SMP.noMsgFlags "hello too" - get b ##> ("", aId, SENT 3) - get a =##> \case ("", c, Msg' 3 PQEncOff "hello too") -> c == bId; _ -> False - ackMessage a bId 3 $ Just "" - liftIO $ noMessages b "no delivery receipt (unsupported version)" - pure (aId, bId) - - disposeAgentClient a - disposeAgentClient b - a' <- getSMPAgentClient' 3 agentCfg {smpAgentVRange = supportedSMPAgentVRange} initAgentServers testDB - b' <- getSMPAgentClient' 4 agentCfg {smpAgentVRange = supportedSMPAgentVRange} initAgentServers testDB2 - - runRight_ $ do - subscribeConnection a' bId - subscribeConnection b' aId - exchangeGreetingsMsgId_ PQEncOff 4 a' bId b' aId - checkVersion a' bId 7 - checkVersion b' aId 7 - (6, PQEncOff) <- A.sendMessage a' bId PQEncOn SMP.noMsgFlags "hello" - get a' ##> ("", bId, SENT 6) - get b' =##> \case ("", c, Msg' 6 PQEncOff "hello") -> c == aId; _ -> False - ackMessage b' aId 6 $ Just "" - get a' =##> \case ("", c, Rcvd 6) -> c == bId; _ -> False - ackMessage a' bId 7 Nothing - (8, PQEncOff) <- A.sendMessage b' aId PQEncOn SMP.noMsgFlags "hello too" - get b' ##> ("", aId, SENT 8) - get a' =##> \case ("", c, Msg' 8 PQEncOff "hello too") -> c == bId; _ -> False - ackMessage a' bId 8 $ Just "" - get b' =##> \case ("", c, Rcvd 8) -> c == aId; _ -> False - ackMessage b' aId 9 Nothing - (10, _) <- A.sendMessage a' bId PQEncOn SMP.noMsgFlags "hello 2" - get a' ##> ("", bId, SENT 10) - get b' =##> \case ("", c, Msg' 10 PQEncOff "hello 2") -> c == aId; _ -> False - ackMessage b' aId 10 $ Just "" - get a' =##> \case ("", c, Rcvd 10) -> c == bId; _ -> False - ackMessage a' bId 11 Nothing - disposeAgentClient a' - disposeAgentClient b' - testDeliveryReceiptsConcurrent :: HasCallStack => (ASrvTransport, AStoreType) -> IO () testDeliveryReceiptsConcurrent (t, msType) = withSmpServerConfigOn t cfg' testPort $ \_ -> do diff --git a/tests/AgentTests/NotificationTests.hs b/tests/AgentTests/NotificationTests.hs index ed6455deb..d81c08a41 100644 --- a/tests/AgentTests/NotificationTests.hs +++ b/tests/AgentTests/NotificationTests.hs @@ -17,7 +17,8 @@ module AgentTests.NotificationTests where -- import Control.Logger.Simple (LogConfig (..), LogLevel (..), setLogLevel, withGlobalLogging) import AgentTests.FunctionalAPITests - ( agentCfgVPrevPQ, + ( agentCfgV7, + agentCfgVPrevPQ, createConnection, exchangeGreetings, get, @@ -28,6 +29,7 @@ import AgentTests.FunctionalAPITests runRight_, sendMessage, switchComplete, + fastSwitchComplete, testServerMatrix2, withAgent, withAgentClients2, @@ -164,10 +166,14 @@ notificationTests ps@(t, _) = do it "should resume batched subscriptions after SMP server is restarted" $ withAPNSMockServer $ \apns -> withNtfServer t $ testNotificationsSMPRestartBatch 50 ps apns - describe "should switch notifications to the new queue" $ + describe "should switch notifications to the new queue (slow rotation)" $ testServerMatrix2 ps $ \servers -> withAPNSMockServer $ \apns -> - withNtfServer t $ testSwitchNotifications servers apns + withNtfServer t $ testSwitchNotifications agentCfgV7 switchComplete servers apns + describe "should switch notifications to the new queue (fast rotation)" $ + testServerMatrix2 ps $ \servers -> + withAPNSMockServer $ \apns -> + withNtfServer t $ testSwitchNotifications agentCfg fastSwitchComplete servers apns it "should keep sending notifications for old token" $ withSmpServer ps $ withAPNSMockServer $ \apns -> @@ -868,9 +874,9 @@ testNotificationsSMPRestartBatch n ps@(t, ASType qsType _) apns = killThread t1 pure res -testSwitchNotifications :: InitialAgentServers -> APNSMockServer -> IO () -testSwitchNotifications servers apns = - withAgentClientsCfgServers2 agentCfg agentCfg servers $ \a b -> runRight_ $ do +testSwitchNotifications :: AgentConfig -> (AgentClient -> ByteString -> AgentClient -> ByteString -> ExceptT AgentErrorType IO ()) -> InitialAgentServers -> APNSMockServer -> IO () +testSwitchNotifications cfg completeSwitch servers apns = + withAgentClientsCfgServers2 cfg cfg servers $ \a b -> runRight_ $ do (aId, bId) <- makeConnection a b exchangeGreetings a bId b aId _ <- registerTestToken a "abcd" NMInstant apns @@ -883,7 +889,7 @@ testSwitchNotifications servers apns = ackMessage a bId msgId Nothing testMessage "hello" _ <- switchConnectionAsync a "" bId - switchComplete a bId b aId + completeSwitch a bId b aId liftIO $ threadDelay 500000 testMessage "hello again" diff --git a/tests/XFTPCLI.hs b/tests/XFTPCLI.hs index ac422c072..3fe2f3bf5 100644 --- a/tests/XFTPCLI.hs +++ b/tests/XFTPCLI.hs @@ -9,10 +9,11 @@ import Simplex.FileTransfer.Client.Main xftpClientDeprecationNotice, ) import Simplex.FileTransfer.Description (kb, mb) +import Simplex.FileTransfer.Util (safeFileNameStr, uniqueCombine) import System.Directory (createDirectoryIfMissing, getFileSize, listDirectory, removeDirectoryRecursive) import System.Environment (withArgs) import System.Exit (ExitCode (ExitSuccess)) -import System.FilePath (()) +import System.FilePath (takeFileName, ()) import System.IO.Silently (capture, capture_) import Test.Hspec hiding (fit, it) import Util @@ -29,6 +30,18 @@ xftpCLIFileTests = around_ testBracket $ do it "should delete file from 2 servers" $ \fsType -> withXFTPServerConfigOn (cfgFS fsType) $ \_ -> withXFTPServerConfigOn (cfgFS2 fsType) $ \_ -> testXFTPCLIDelete_ it "prepareChunkSizes should use 2 chunk sizes" $ \_ -> testPrepareChunkSizes + describe "received file name" $ do + it "sanitizes any name to a real file name" $ \_ -> + filter (not . sanitized) fileNames `shouldBe` [] + it "sanitizes to a name with no directory components" $ \_ -> + filter (not . bareName) fileNames `shouldBe` [] + it "combines a sanitized name inside the destination folder" $ \_ -> + testReceivedFileNameCombine + where + fileNames :: [FilePath] + fileNames = ["", ".", "..", "...", "../x", "../../etc/passwd", "/etc/cron.d/x", "a/b", "x/", "test.pdf", ".hidden", "a b.tar.gz"] + sanitized n = let n' = safeFileNameStr n in n' /= "" && n' /= "." && n' /= ".." + bareName n = let n' = safeFileNameStr n in n' == takeFileName n' testBracket :: IO () -> IO () testBracket = @@ -162,6 +175,10 @@ testXFTPCLIDelete_ = do xftpCLI ["recv", fdRcv2, recipientFiles, "--tmp=tests/tmp"] `shouldThrow` anyException +testReceivedFileNameCombine :: IO () +testReceivedFileNameCombine = + uniqueCombine recipientFiles "../../escaped.txt" `shouldReturn` (recipientFiles "escaped.txt") + testPrepareChunkSizes :: IO () testPrepareChunkSizes = do prepareChunkSizes (mb 9 + kb 256) `shouldBe` [mb 4, mb 4, mb 1, mb 1]