You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: spec/modules/README.md
+23Lines changed: 23 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -52,6 +52,21 @@ Things that would surprise a competent Haskell developer reading the code for th
52
52
- Alternatives considered and rejected
53
53
- Known limitations and their justification
54
54
55
+
## Non-obvious threshold
56
+
57
+
The guiding principle: **non-obvious state machines and flows require documentation; standard things don't.**
58
+
59
+
Document:
60
+
- Multi-step protocols and negotiation flows (e.g., KEM propose/accept round-trips)
61
+
- Monotonic or irreversible state transitions (e.g., PQ support can only be enabled, never disabled)
62
+
- Silent error behaviors (e.g., `verify` returns `False` on algorithm mismatch instead of an error)
63
+
- Design rationale for non-standard choices (e.g., why byte-reverse a nonce, why hash-then-encrypt for authenticators)
64
+
65
+
Do NOT document:
66
+
- Standard algorithm properties (e.g., Ed25519 public key derivable from private key)
67
+
- Well-known protocol mechanics (e.g., HKDF usage per RFC 5869, deterministic nonce derivation in double ratchet)
68
+
- Implementation details that follow directly from the type signatures
69
+
55
70
## What NOT to include
56
71
57
72
-**Type signatures** — the code has them
@@ -107,6 +122,14 @@ This is valuable — it confirms someone looked and found nothing to document.
107
122
108
123
## Linking conventions
109
124
125
+
### Module doc → protocol docs
126
+
When a module implements or is governed by a protocol specification in `protocol/`, link to it near the top of the module doc (after the overview). Do not duplicate protocol content — just reference it:
This is especially important for modules in transport, protocol, client, server, and agent layers where behavior is defined by the protocol spec rather than being self-evident from the code.
Copy file name to clipboardExpand all lines: spec/modules/Simplex/Messaging/Crypto.md
+16-4Lines changed: 16 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -55,10 +55,6 @@ Both apply `pad`/`unPad` by default. The `NoPad` variants skip padding.
55
55
56
56
The XSalsa20 implementation splits the 24-byte nonce into two 8-byte halves. The first half initializes the cipher state (prepended with 16 zero bytes), the second derives a subkey. The first 32 bytes of output become the Poly1305 one-time key (`rs`), then the rest encrypts the message. This is the standard NaCl construction.
57
57
58
-
## CbAuthenticator
59
-
60
-
An authentication scheme that encrypts the SHA-512 hash of the message using crypto_box, rather than the message itself. The result is 80 bytes (64 hash + 16 auth tag). Used for authenticating messages where the content is transmitted separately from the authentication proof.
61
-
62
58
## Secret box chains (sbcInit / sbcHkdf)
63
59
64
60
HKDF-based key chains for deriving sequential key+nonce pairs:
@@ -77,6 +73,22 @@ All keys are encoded as ASN.1 DER (X.509 SubjectPublicKeyInfo for public, PKCS#8
77
73
78
74
`GCMIV` constructor is not exported — only `gcmIV :: ByteString -> Either CryptoError GCMIV` is available, which validates that the input is exactly 12 bytes. This prevents construction of invalid IVs.
79
75
76
+
## verify silently returns False on algorithm mismatch
77
+
78
+
`verify :: APublicVerifyKey -> ASignature -> ByteString -> Bool` uses `testEquality` on the algorithm singletons. If the key is Ed25519 but the signature is Ed448 (or vice versa), `testEquality` fails and `verify` returns `False` — no error, no indication of a type mismatch. A correctly-formed signature can "fail" simply because the wrong algorithm key was passed.
79
+
80
+
## dh' returns raw DH output — no key derivation
81
+
82
+
`dh'` returns the raw X25519/X448 shared point with no hashing or HKDF. Callers must apply their own KDF: [SNTRUP761](./Crypto/SNTRUP761.md) hashes with SHA3-256, the [ratchet](./Crypto/Ratchet.md#kdf-functions) uses HKDF-SHA512. Not all DH libraries behave this way — some hash the output automatically.
83
+
84
+
## reverseNonce
85
+
86
+
`reverseNonce` creates a "reply" nonce by byte-reversing the original 24-byte nonce. Used for bidirectional communication where both sides need distinct nonces derived from the same starting value. The two nonces are guaranteed distinct unless the original is a byte palindrome, which is astronomically unlikely for random 24-byte values.
87
+
88
+
## CbAuthenticator
89
+
90
+
An authentication scheme that encrypts the SHA-512 hash of the message using crypto_box, rather than the message itself. The result is 80 bytes (64 hash + 16 auth tag). This is the djb-recommended authenticator scheme: it proves knowledge of the shared secret and the message content, without requiring the message to fit in a single crypto_box, and without revealing message content even to someone who compromises the shared key after verification.
91
+
80
92
## generateKeyPair is STM
81
93
82
94
Key generation uses `TVar ChaChaDRG` and runs in `STM`, not `IO`. This allows key generation inside `atomically` blocks, which is used extensively in handshake and ratchet initialization code.
`pqX3dhSnd` / `pqX3dhRcv` perform the extended X3DH:
@@ -46,9 +48,13 @@ Each message header carries `msgMaxVersion` (the sender's max supported ratchet
46
48
47
49
`largeP` detects the length-prefix format by peeking at the first byte: if < 32, it's a 2-byte `Large` prefix (new format); otherwise it's a 1-byte prefix (old format). This allows upgrading the header encoding format in a single message without a version bump.
48
50
51
+
## maxSkip = 512 — DoS protection
52
+
53
+
`maxSkip` is a hardcoded constant (not configurable). Messages claiming to be more than 512 positions ahead of the current counter are rejected with `CERatchetTooManySkipped`. This prevents an attacker from forcing the receiver to compute and store an unbounded number of skipped message keys.
54
+
49
55
## Skipped message keys
50
56
51
-
When messages arrive out of order, the ratchet computes and stores the message keys for skipped messages (up to `maxSkip = 512`). Skipped keys are stored in a `Map HeaderKey (Map Word32 MessageKey)` — keyed first by header key, then by message number.
57
+
When messages arrive out of order, the ratchet computes and stores the message keys for skipped messages (up to `maxSkip`). Skipped keys are stored in a `Map HeaderKey (Map Word32 MessageKey)` — keyed first by header key, then by message number.
52
58
53
59
The `SkippedMsgDiff` type represents changes to the skipped key store as a diff rather than a full replacement — this is persisted to the database, and the full state is loaded for the next message. `applySMDiff` is only used in tests.
54
60
@@ -61,6 +67,14 @@ Decryption tries three strategies in order:
61
67
62
68
If strategy 1 decrypts the header but the message number isn't in skipped keys, it checks whether this header key corresponds to the current or next ratchet to decide whether to advance.
63
69
70
+
### decryptSkipped — linear scan through all stored header keys
71
+
72
+
`decryptSkipped` iterates through ALL `(HeaderKey, SkippedHdrMsgKeys)` pairs, attempting header decryption with each key. When header decryption succeeds but the message number is NOT in the skipped keys for that header, the result is `SMHeader` — which includes whether the key matches the current ratchet (`rcHKr` → `SameRatchet`) or the next ratchet (`rcNHKr` → `AdvanceRatchet`). This falls through to normal decryption processing rather than producing an error.
73
+
74
+
### decryptMessage — ratchet advances even on failure
75
+
76
+
`decryptMessage` returns `Either CryptoError ByteString` inside the `ExceptT` monad — a message decryption failure does NOT abort the ratchet state update. The ratchet counter advances (`rcNr + 1`) and chain key updates (`rcCKr'`) regardless of whether the message body decrypts successfully. This preserves ratchet state consistency for retransmission and error recovery.
77
+
64
78
## rcEncryptHeader — separated from rcEncryptMsg
65
79
66
80
Encryption is split into two steps: `rcEncryptHeader` produces a `MsgEncryptKey` (containing the encrypted header and message key), then `rcEncryptMsg` uses that key to encrypt the message body. This separation allows the ratchet state to be updated (persisted) before the message is encrypted, which is important for crash recovery — if the process crashes after encrypting but before sending, the ratchet state must already reflect the advanced counter.
@@ -80,6 +94,19 @@ Two distinct newtypes with identical structure (`Bool` wrapper):
80
94
-`PQSupport`: whether PQ **can** be used (determines header padding size, cannot be disabled once enabled)
81
95
-`PQEncryption`: whether PQ **is** being used for the current send/receive ratchet
82
96
97
+
### pqEnableSupport is monotonic
98
+
99
+
`pqEnableSupport v sup enc = PQSupport $ sup || (v >= pqRatchetE2EEncryptVersion && enc)`. The `||` means once PQ support is `True`, it stays `True` regardless of subsequent messages. PQ encryption (usage) can be toggled per-message; PQ support (capability / header size) only ratchets up. This prevents the larger header format from being downgraded once negotiated.
100
+
101
+
## replyKEM_ — two-step KEM negotiation
102
+
103
+
KEM establishment requires two message round-trips, as described in the [PQDR KEM state machine](../../../../protocol/pqdr.md#kem-state-machine):
104
+
105
+
1.**Propose**: if the sender has no KEM in their header but the replier supports PQ at sufficient version, the replier includes a KEM proposal (`RKParamsProposed` — their encapsulation public key)
106
+
2.**Accept**: if the sender proposed KEM, the replier accepts by encapsulating against the proposed key and including the ciphertext + their own new encapsulation key (`RKParamsAccepted`)
107
+
108
+
After acceptance, both sides have a shared KEM secret that is folded into the root KDF. Subsequent ratchet steps continue the KEM exchange with fresh keypairs on each side.
109
+
83
110
## Error semantics
84
111
85
112
-`CERatchetEarlierMessage n`: message number is `n` positions before the next expected (already processed or skipped-and-consumed)
0 commit comments