mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-03-30 14:16:00 +00:00
fixes
This commit is contained in:
@@ -37,7 +37,7 @@ Connection creation is split into two phases to satisfy both design constraints:
|
||||
|
||||
**`prepareConnectionLink`** (no network, no database): generates root Ed25519 signing key pair and queue-level X25519 DH keys. Derives a short link key as `SHA3_256` of the encoded fixed link data. Returns the connection link URI and `PreparedLinkParams` in memory. The application can now embed the link in link data (e.g., for short link resolution) before the queue exists.
|
||||
|
||||
**`createConnectionForLink`** (single network call): uses the prepared parameters to create the queue on the router with SKEY (root signature). The sender ID is deterministically derived from the correlation nonce (`SMP.EntityId $ B.take 24 $ C.sha3_384 corrId`), so a lost response can be retried - the router validates the same sender ID.
|
||||
**`createConnectionForLink`** (single network call): uses the prepared parameters to create the queue on the router with NEW (root signing key as owner auth). The sender ID is deterministically derived from the correlation nonce (`SMP.EntityId $ B.take 24 $ C.sha3_384 corrId`), so a lost response can be retried - the router validates the same sender ID.
|
||||
|
||||
Without split-phase, the application would need to create the queue first, get the link, then update the queue with link data containing the link - requiring an extra round-trip.
|
||||
|
||||
@@ -83,7 +83,7 @@ Queue rotation replaces a receive queue with a new one on a different router, pr
|
||||
|
||||
### Protocol sequence
|
||||
|
||||
Rotation is initiated by `switchConnectionAsync` (client API) or by receiving QADD from the peer. Preconditions: connection must be duplex, no switch already in progress, ratchet must not be syncing.
|
||||
Rotation is initiated by the switching party calling `switchConnectionAsync` (client API), which sends QADD. The peer responds to QADD by creating a new send queue and replying with QKEY. Preconditions: connection must be duplex, no switch already in progress, ratchet must not be syncing.
|
||||
|
||||
```
|
||||
Receiver (switching party) Sender (peer)
|
||||
@@ -157,7 +157,7 @@ Both parties compute `rkHash = SHA256(pubKeyBytes k1 || pubKeyBytes k2)` for the
|
||||
|
||||
### EREADY completion
|
||||
|
||||
`EREADY` carries `lastExternalSndId` - the ID of the last message sent with the old ratchet. The receiving party uses this to know when the old ratchet's messages are exhausted and the new ratchet is fully active. Until EREADY arrives, messages may arrive encrypted with either the old or new ratchet.
|
||||
`EREADY` carries `lastExternalSndId` - the ID of the last message the sender received from the peer before switching ratchets. The receiving party uses this to know when the old ratchet's messages are exhausted and the new ratchet is fully active. Until EREADY arrives, messages may arrive encrypted with either the old or new ratchet.
|
||||
|
||||
### Error recovery
|
||||
|
||||
@@ -179,17 +179,17 @@ Four variants with single-character discriminants:
|
||||
|
||||
| Variant | Disc. | Encryption | When |
|
||||
|---------|-------|-----------|------|
|
||||
| `AgentConfirmation` | `'C'` | Per-queue E2E only | Connection handshake |
|
||||
| `AgentConfirmation` | `'C'` | Per-queue E2E (outer) + double ratchet (inner `encConnInfo`) | Connection handshake |
|
||||
| `AgentMsgEnvelope` | `'M'` | Double ratchet | Normal messages |
|
||||
| `AgentInvitation` | `'I'` | Per-queue E2E only | Contact URI join |
|
||||
| `AgentRatchetKey` | `'R'` | Per-queue E2E only | Ratchet sync |
|
||||
|
||||
Only `AgentMsgEnvelope` is double-ratchet encrypted. The other three use only the per-queue E2E encryption (DH shared secret from queue creation). This is because during handshake and ratchet sync, the double ratchet is either not yet established or being replaced.
|
||||
`AgentMsgEnvelope` is fully double-ratchet encrypted. `AgentConfirmation` uses per-queue E2E for the outer envelope but also contains `encConnInfo` which is double-ratchet encrypted (the ratchet is initialized during confirmation processing). `AgentInvitation` and `AgentRatchetKey` use only per-queue E2E - the double ratchet is either not yet established or being replaced.
|
||||
|
||||
### Level 2: AgentMessage (application)
|
||||
|
||||
Inside the decrypted envelope:
|
||||
- `AgentConnInfo` / `AgentConnInfoReply` - connection info during handshake (not double-ratchet encrypted)
|
||||
- `AgentConnInfo` / `AgentConnInfoReply` - connection info during handshake (double-ratchet encrypted inside `encConnInfo`)
|
||||
- `AgentRatchetInfo` - ratchet sync payload (not double-ratchet encrypted)
|
||||
- `AgentMessage APrivHeader AMessage` - user and control messages (double-ratchet encrypted)
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ All agent background processing - async commands, message delivery, notification
|
||||
|
||||
**Error classification**: `withWork` distinguishes two failure modes:
|
||||
- *Work-item error* (`isWorkItemError`): the task itself is broken (likely recurring). Worker stops and sends `CRITICAL False`.
|
||||
- *Store error*: transient database issue. Worker re-signals `doWork` and reports `INTERNAL` (retry may succeed).
|
||||
- *Other error*: any non-work-item error (e.g., transient database issue). Worker re-signals `doWork` and reports `INTERNAL` (retry may succeed).
|
||||
|
||||
**Restart rate limiting**: On worker exit, `restartOrDelete` checks the `restarts` counter against `maxWorkerRestartsPerMin`. Under the limit: reset action, re-signal, restart. Over the limit: delete the worker from the map and send `CRITICAL True` (escalation to the application). A restart only proceeds if the `workerId` in the map still matches the current worker - a stale restart from a replaced worker is a no-op.
|
||||
|
||||
@@ -54,7 +54,7 @@ Async commands handle state transitions that require network calls but shouldn't
|
||||
- *Client commands* (`AClientCommand`): `NEW`, `JOIN`, `LET` (allow connection), `ACK`, `LSET`/`LGET` (set/get connection link data), `SWCH` (switch queue), `DEL`. Triggered by application API calls.
|
||||
- *Internal commands* (`AInternalCommand`): `ICAck` (ack to router), `ICAckDel` (ack + delete local message), `ICAllowSecure`/`ICDuplexSecure` (secure after confirmation), `ICQSecure` (secure queue during switch), `ICQDelete` (delete old queue after switch), `ICDeleteConn` (delete connection), `ICDeleteRcvQueue` (delete specific receive queue). Generated *during* message processing to handle state transitions asynchronously.
|
||||
|
||||
**Retry and movement**: `tryMoveableCommand` wraps execution with `withRetryInterval`. On `temporaryOrHostError`, it retries with backoff. On cross-server errors (e.g., queue moved to different router), it updates the command's server field in the store (`CCMoved`) and retries against the new server.
|
||||
**Retry and movement**: `tryMoveableCommand` wraps execution with `withRetryInterval`. On `temporaryOrHostError`, it retries with backoff. Individual command handlers can return `CCMoved` (e.g., when a queue has moved to a different router) after updating the command's server field in the store - `tryMoveableCommand` then exits cleanly, letting the moved command be picked up by the appropriate worker.
|
||||
|
||||
**Locking**: State-sensitive commands use `tryWithLock` / `tryMoveableWithLock`, which acquire `withConnLock` before execution. This serializes operations on the same connection, preventing races between concurrent command processing and message receipt.
|
||||
|
||||
@@ -73,8 +73,7 @@ Message delivery uses a split-phase encryption design: the ratchet advances in t
|
||||
2. Call `agentRatchetEncryptHeader` - advances the double ratchet, produces a message encryption key (MEK), padded length, and PQ encryption status
|
||||
3. Store `SndMsg` with `SndMsgPrepData` (MEK, paddedLen, sndMsgBodyId) in the database
|
||||
4. Create `SndMsgDelivery` record for each send queue
|
||||
5. Increment `msgDeliveryOp.opsInProgress` (for suspension tracking)
|
||||
6. Signal delivery workers via `getDeliveryWorker`
|
||||
5. `submitPendingMsg` - increments `msgDeliveryOp.opsInProgress` (for suspension tracking) and signals delivery workers via `getDeliveryWorker`
|
||||
|
||||
**Phase 2 - delivery worker** (`runSmpQueueMsgDelivery`):
|
||||
1. `throwWhenNoDelivery` - kills the worker thread if the queue's address has been removed from `smpDeliveryWorkers` (prevents delivery to queues replaced during switch)
|
||||
@@ -82,9 +81,9 @@ Message delivery uses a split-phase encryption design: the ratchet advances in t
|
||||
3. Re-encode the message with `internalSndId`/`prevMsgHash`, then `rcEncryptMsg` to encrypt with the stored MEK (no ratchet access needed)
|
||||
4. `sendAgentMessage` - per-queue encrypt + SEND to the router
|
||||
|
||||
**Connection info messages** (`AM_CONN_INFO`, `AM_CONN_INFO_REPLY`) skip split-phase encryption entirely - they are sent as plaintext confirmation bodies via `sendConfirmation`.
|
||||
**Connection info messages** (`AM_CONN_INFO`, `AM_CONN_INFO_REPLY`) skip split-phase encryption entirely - they are sent as per-queue E2E encrypted confirmation bodies via `sendConfirmation` (encrypted with `agentCbEncrypt`, not with the double ratchet).
|
||||
|
||||
**Retry with dual intervals**: Delivery uses `withRetryLock2`, which maintains two independent retry clocks (slow and fast). A background thread sleeps for the current interval, then signals the delivery worker via `tryPutTMVar`. When the router sends `QCONT` (queue buffer cleared), the agent calls `tryPutTMVar retryLock ()` to wake the delivery thread immediately, avoiding unnecessary delay.
|
||||
**Retry with dual intervals**: Delivery uses `withRetryLock2`, which maintains two retry interval states (slow and fast) but only one wait is active at a time. A background thread sleeps for the current interval, then signals the delivery worker via `tryPutTMVar`. When the router sends `QCONT` (queue buffer cleared), the agent calls `tryPutTMVar retryLock ()` to wake the delivery thread immediately, avoiding unnecessary delay.
|
||||
|
||||
**Error handling**:
|
||||
- `SMP QUOTA` - switch to slow retry, don't penalize (backpressure from router)
|
||||
@@ -115,7 +114,7 @@ SessSubs
|
||||
|
||||
- **Active → Pending**: When `setSessionId` is called with a *different* session ID (TLS reconnect), all active subscriptions are atomically demoted to pending. Session ID is updated to the new value.
|
||||
|
||||
- **Pending → Removed**: `failSubscriptions` moves permanently-failed queues (non-temporary SMP errors) to `removedSubs`. The removal is tracked for diagnostic reporting via `getSubscriptions`.
|
||||
- **Pending → Removed**: `failSubscriptions` moves permanently-failed queues (non-temporary SMP errors) to `removedSubs` - a separate `TMap` in `AgentClient`, not part of `TSessionSubs`. The removal is tracked for diagnostic reporting via `getSubscriptions`.
|
||||
|
||||
**Service-associated queues**: Queues with `serviceAssoc=True` are *not* added to `activeSubs` individually. Instead, the service subscription's count is incremented and its `idsHash` XOR-accumulates the queue's hash. The router tracks individual queues via the service subscription; the agent only tracks the aggregate. Consequence: `hasActiveSub(rId)` returns `False` for service-associated queues - callers must check the service subscription separately.
|
||||
|
||||
|
||||
@@ -23,18 +23,18 @@ The handshake spans `Client.connectRCHost` (controller side, despite the name),
|
||||
|
||||
2. **Invitation delivery**: the invitation reaches the host either out-of-band (QR code scan for first pairing) or via encrypted multicast announcement (subsequent sessions - see [Multicast discovery](#multicast-discovery)).
|
||||
|
||||
3. **Host connects via TLS**: `connectRCCtrl` establishes a TLS connection. Both sides validate 2-certificate chains (leaf + CA root). On reconnection, the host validates the controller's CA fingerprint against `KnownHostPairing`; on first pairing, it stores the fingerprint.
|
||||
3. **Host connects via TLS**: `connectRCCtrl` establishes a TLS connection. Both sides validate certificate chains. On the controller side, `onClientCertificate` explicitly checks for a 2-certificate chain (leaf + CA root) and validates the host's CA fingerprint against `KnownHostPairing.hostFingerprint` (or stores it on first pairing). On the host side, the controller's CA fingerprint is validated against `RCCtrlPairing.ctrlFingerprint` in `updateCtrlPairing`.
|
||||
|
||||
4. **User confirmation barrier**: after TLS connects, the controller extracts the TLS channel binding (`tlsUniq`) as a session code. The application displays this code; the user verifies it on the host. `confirmCtrlSession` uses a double `putTMVar` - the first put signals the decision (accept/reject), the second blocks until the session thread consumes it, creating a synchronization point that prevents the session from proceeding before confirmation completes.
|
||||
4. **User confirmation barrier**: after TLS connects, both sides extract the TLS channel binding (`tlsUniq`) as a session code. The application displays this code on both devices for the user to verify. On the host side, `confirmCtrlSession` uses a double `putTMVar` - the first put signals the decision (accept/reject), the second blocks until the session thread acknowledges the value, ensuring `confirmCtrlSession` does not return prematurely.
|
||||
|
||||
5. **Hello exchange** (asymmetric encryption):
|
||||
- Controller sends `RCHostEncHello`: DH public key in plaintext + encrypted body containing the KEM encapsulation key, CA fingerprint, and app info. Encrypted with `cbEncrypt` (classical DH secret).
|
||||
- Host decrypts the hello, performs KEM encapsulation (see [KEM hybrid key exchange](#kem-hybrid-key-exchange)), derives the hybrid session key, and sends `RCCtrlEncHello` encrypted with `sbEncrypt` (post-quantum hybrid key).
|
||||
- The asymmetry is deliberate: at the time the controller sends its hello, KEM hasn't completed yet, so only classical DH encryption is available. After the host encapsulates, both sides have the hybrid key.
|
||||
- Host sends `RCHostEncHello` (`prepareHostHello`): DH public key in plaintext + encrypted body containing the KEM encapsulation key, CA fingerprint, and app info. Encrypted with `cbEncrypt` (classical DH secret).
|
||||
- Controller decrypts the hello, performs KEM encapsulation (see [KEM hybrid key exchange](#kem-hybrid-key-exchange)), derives the hybrid session key, initializes a chain via `sbcInit`, and sends `RCCtrlEncHello` (`prepareHostSession`) encrypted with a key derived from the chain (`sbcHkdf` + `sbEncrypt`).
|
||||
- The asymmetry is deliberate: at the time the host sends its hello, KEM hasn't completed yet, so only classical DH encryption is available. After the controller encapsulates, both sides have the hybrid key.
|
||||
|
||||
6. **Chain key initialization**: both sides call `sbcInit` with the hybrid key to derive send/receive chain keys. The controller explicitly **swaps** the key pair (`swap` call in `prepareCtrlSession`) - both sides derive keys in the same order from `sbcInit`, but have opposite send/receive roles, so the controller must reverse them. The host does not swap.
|
||||
6. **Chain key initialization**: both sides call `sbcInit` with the hybrid key to derive send/receive chain keys. The host explicitly **swaps** the key pair (`swap` call in `prepareCtrlSession`, which runs on the host side despite its name) - both sides derive keys in the same order from `sbcInit`, but have opposite send/receive roles, so the host must reverse them. The controller does not swap.
|
||||
|
||||
7. **Error path**: if KEM encapsulation fails, the host sends `RCCtrlEncError` encrypted with the DH key (not the hybrid key, which doesn't exist yet). The controller can decrypt the error because it has the DH secret from step 5.
|
||||
7. **Error path**: if KEM encapsulation fails, the controller sends `RCCtrlEncError` (a variant of `RCCtrlEncHello`) encrypted with the DH key (not the hybrid key, which doesn't exist yet). The host can decrypt the error because it has the DH secret from step 5. Note: this error path is not yet fully implemented in the code.
|
||||
|
||||
---
|
||||
|
||||
@@ -42,23 +42,20 @@ The handshake spans `Client.connectRCHost` (controller side, despite the name),
|
||||
|
||||
**Source**: [RemoteControl/Client.hs](../../src/Simplex/RemoteControl/Client.hs)
|
||||
|
||||
The session key combines classical Diffie-Hellman with SNTRUP761 (lattice-based KEM) via `SHA3_256(dhSecret || kemSharedKey)` (`kemHybridSecret` in Client.hs). This provides protection against quantum computers while maintaining classical security as a fallback.
|
||||
The session key combines classical Diffie-Hellman with SNTRUP761 (lattice-based KEM) via `SHA3_256(dhSecret || kemSharedKey)` (`kemHybridSecret` in `Crypto/SNTRUP761.hs`). This provides protection against quantum computers while maintaining classical security as a fallback.
|
||||
|
||||
**First session** - KEM public key is too large for a QR code invitation, so it travels in the encrypted hello body:
|
||||
The KEM public key is too large for a QR code invitation, so it travels in the encrypted hello body. Fresh KEM keys are generated every session - no KEM state is cached between sessions.
|
||||
|
||||
1. Controller generates DH + KEM key pairs, puts KEM encapsulation key in the hello body
|
||||
2. Host decrypts hello with DH secret, extracts KEM encapsulation key
|
||||
3. Host encapsulates: produces `(kemCiphertext, kemSharedKey)`
|
||||
4. Host derives hybrid key: `SHA3_256(dhSecret || kemSharedKey)`
|
||||
5. Host sends `kemCiphertext` in the controller hello body
|
||||
6. Controller decapsulates `kemCiphertext` to recover `kemSharedKey`, derives the same hybrid key
|
||||
1. Host generates a fresh KEM key pair (`prepareHostHello`), puts the KEM public key in the host hello body
|
||||
2. Controller decrypts hello with DH secret, extracts KEM public key
|
||||
3. Controller encapsulates (`sntrup761Enc`): produces `(kemCiphertext, kemSharedKey)`
|
||||
4. Controller derives hybrid key: `SHA3_256(dhSecret || kemSharedKey)`
|
||||
5. Controller sends `kemCiphertext` in the ctrl hello body (`RCCtrlEncHello`)
|
||||
6. Host decapsulates `kemCiphertext` (`sntrup761Dec`) to recover `kemSharedKey`, derives the same hybrid key
|
||||
|
||||
**Subsequent sessions** (via multicast) - the previous session's KEM secret is cached in the pairing:
|
||||
The KEM exchange is identical for first and subsequent sessions. The only difference between sessions is how the invitation is delivered (QR code vs multicast) and whether TLS fingerprints are stored for the first time or verified against known pairings.
|
||||
|
||||
- Both sides already know each other's KEM capabilities from the previous session
|
||||
- Fresh DH keys are generated per session for forward secrecy
|
||||
- The hybrid key derivation uses the new DH secret + the cached KEM secret
|
||||
- `updateKnownHost` (called in `prepareHostSession`) updates the stored DH public key for the next session
|
||||
`updateKnownHost` (called in `prepareHostSession` on the controller) updates the stored host DH public key (`hostDhPubKey` in `KnownHostPairing`) - this is used for encrypting multicast announcements in subsequent sessions, not for KEM.
|
||||
|
||||
**Key rotation and `prevDhPrivKey`**: when the host updates its DH key pair for a new session, it retains the previous private key in `RCCtrlPairing.prevDhPrivKey`. This is critical for multicast - during the transition window, the controller may send announcements encrypted with the old public key. `findRCCtrlPairing` tries decryption with both the current and previous DH keys. Without this fallback, key rotation would break multicast discovery.
|
||||
|
||||
@@ -68,23 +65,24 @@ The session key combines classical Diffie-Hellman with SNTRUP761 (lattice-based
|
||||
|
||||
**Source**: [RemoteControl/Client.hs](../../src/Simplex/RemoteControl/Client.hs), [RemoteControl/Invitation.hs](../../src/Simplex/RemoteControl/Invitation.hs), [RemoteControl/Discovery.hs](../../src/Simplex/RemoteControl/Discovery.hs)
|
||||
|
||||
For subsequent sessions (after initial QR pairing), the controller announces its presence via UDP multicast so the host can connect without scanning a new QR code. The flow spans `Client.announceRC`, `Client.discoverRCCtrl`, `Client.findRCCtrlPairing`, `Invitation.signInvitation`/`verifySignedInvitation`, and `Discovery.joinMulticast`/`withSender`.
|
||||
For subsequent sessions (after initial QR pairing), the controller announces its presence via UDP multicast so the host can connect without scanning a new QR code. The flow spans `Client.announceRC`, `Client.discoverRCCtrl`, `Client.findRCCtrlPairing`, `Invitation.signInvitation`/`verifySignedInvitation`, and `Discovery.withListener`/`withSender`.
|
||||
|
||||
**Announcement creation** (`announceRC`):
|
||||
|
||||
1. The invitation is signed with a dual-signature chain: the session key signs the invitation URI, then the identity key signs the URI + session signature concatenated. This chain means a compromised session key alone cannot forge a valid identity-signed announcement - the identity key must also be compromised.
|
||||
1. The invitation is signed with a dual-signature chain: the session key signs the invitation URI, then the identity key signs the concatenation `URI + "&ssig=" + sessionSignature`. This chain means a compromised session key alone cannot forge a valid identity-signed announcement - the identity key must also be compromised.
|
||||
2. The signed invitation is encrypted with a DH shared secret between the host's known DH public key and the controller's ephemeral DH private key.
|
||||
3. The encrypted packet is padded to 900 bytes (privacy: all announcements are indistinguishable by size).
|
||||
4. Sent 60 times at 1-second intervals to multicast group `224.0.0.251:5227`.
|
||||
5. Runs as a cancellable async task - cancelled in `prepareHostSession` once the session is established.
|
||||
5. Runs as a cancellable async task - cancelled in `connectRCHost` after `prepareHostSession` returns, once the session is established.
|
||||
|
||||
**Listener and discovery** (`discoverRCCtrl`):
|
||||
|
||||
1. Host calls `joinMulticast` to subscribe to the multicast group. A shared `TMVar Int` counter tracks active listeners - OS-level `IP_ADD_MEMBERSHIP` is only issued on 0→1 transition, `IP_DROP_MEMBERSHIP` on 1→0. This prevents duplicate syscalls when multiple listeners are active.
|
||||
2. For each received packet, `findRCCtrlPairing` iterates over known pairings and tries decryption with the current DH key, falling back to `prevDhPrivKey` if present.
|
||||
3. After successful decryption, the invitation's `dh` field is verified against the announcement's `dhPubKey` to prevent relay attacks.
|
||||
4. Dual signatures are verified: session signature first, then identity signature.
|
||||
5. 30-second timeout on the entire discovery process (`RCENotDiscovered` on expiry).
|
||||
4. The source IP address is checked against the invitation's `host` field - prevents re-broadcasting a legitimate announcement from a different host.
|
||||
5. Dual signatures are verified: session signature first, then identity signature.
|
||||
6. 30-second timeout on the entire discovery process (`RCENotDiscovered` on expiry).
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user