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 @@
-
\ No newline at end of file
+
\ 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]