Skip to content

Commit eb55742

Browse files
authored
Update STRANDLOCK_PROTOCOL.md
1 parent ef9dbc5 commit eb55742

File tree

1 file changed

+78
-33
lines changed

1 file changed

+78
-33
lines changed

STRANDLOCK_PROTOCOL.md

Lines changed: 78 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Strandlock Protocol Specification
22
Version: **`1.0`**
33

4-
Date: **`2025-09-08`**
4+
Date: **`2025-09-25`**
55

66
Author(s): **`ChadSec1`** (**`Freedom Club Sec`**)
77

@@ -15,14 +15,20 @@ Intended Audience: Security engineers, cryptographers, protocol implementers
1515

1616
The *`Strandlock`* Protocol is a composite encryption protocol designed to intertwine multiple cryptographic primitives to achieve robust security. Its purpose is to ensure that the compromise of one, two, or even three different cryptographic primitives does not jeopardize the confidentiality or integrity of messages.
1717

18-
Even if `ML-KEM-1024` and `Classic McEliece-8192128` are broken, messages remain secure, provided that the initial `SMP` verification request is not intercepted. If the initial SMP request is intercepted, security is maintained as long as the SMP answer retains sufficient entropy.
18+
The protocol retains high-availability asynchronous behaviour, without reducing security and or privacy like similar protocols.
19+
20+
21+
If `ML-KEM-1024` and `Classic McEliece-8192128` are broken, messages remain secure, provided that the initial `SMP` verification request is not intercepted. If the initial SMP request is intercepted, security is maintained as long as the SMP answer retains sufficient entropy.
1922

2023
If `xChaCha20poly1305` is broken, messages remain safe as long as (at least) one `KEM` is uncompromised.
2124

2225
If `OTP` implementation has mistakes, messages remain safe as long as `xChaCha20Poly1305` is remains unbroken.
2326

2427
If Both `KEMs`, and `xChaCha20Poly1305` are compromised in future, as long as `OTP batch` request was not intercepted nor logged, messages remain safe.
2528

29+
All cryptographic primitives are not just stacked on top of each other, but interwined. Each primitive both aids each other, and acts as a fallback if one or more are broken.
30+
31+
2632
*`Strandlock`* is transport-agnostic. It can operate over any underlying protocol, including federated chat systems like *`Coldwire`* or raw `TCP` sockets.
2733

2834
### 1. Terminology
@@ -43,7 +49,7 @@ A logical state maintained between two parties that tracks shared secrets, nonce
4349
A cryptographically secure random value used for entropy injection, whitening, and rotation. Strand nonces are exclusively used for `xChaCha20Poly1305` wrapping encryption. Every request contains the next nonce that will be used for the next request. Such nonces we call "Strand Nonces". Alice and Bob both save each other strand nonces.
4450

4551
#### Strand Key
46-
Any key material derived, rotated, or combined from multiple primitives within Strandlock. Strand Key is fed to `xChaCha20Poly1305` to "wrap encrypt" everything. Applies to PFS, MSGS requests and responses, but not SMP. SMP process uses a temporary key simply dubbed "Temporary xChaCha key" or "SMP key"
52+
Any key material derived, rotated, or combined from multiple primitives within Strandlock. Strand Key is fed to `xChaCha20Poly1305` to "wrap encrypt" everything. Applies to `SMP` (step 3 and onward), `PFS`, `MSGS` requests and responses.
4753

4854

4955
#### SMP (Socialist Millionaire Protocol)
@@ -58,17 +64,39 @@ A collection of one-time pad material derived from multiple primitives and signe
5864
#### Request Types:
5965
Every request includes a message type identifier, which may be visible only in the very first `SMP` stage request. For all other requests that come afterwards, the type is encrypted within the payload.
6066

61-
#### Nonces:
62-
Each request contains a `24-bytes nonce` immediately following the `type` field. These `nonces` are meant to be used for the next request to prevent metadata leakage, replay attacks, and on the small off-chance that a randomly generated nonce repeats twice, network adversaries wouldn't know a nonce reuse occured.
63-
64-
Additionally, while all public security proofs of `xChaCha20Poly1305` assume nonce is public, encrypting and or hiding the `nonce` might actually "future-proof" `xChaCha20Poly1305` against potentinal future attacks, leaving only open window through pure dumb brute-forcing of `32 bytes` key space.
6567

6668
#### Encryption:
67-
All payloads are encrypted with `XChaCha20Poly1305`, except for the `SMP` initiation stage.
69+
All payloads are encrypted with `XChaCha20Poly1305`, except for `SMP` step 1 (initiation).
70+
71+
#### Forward ratchet and Nonces:
72+
All data is "wrap encrypted" using `xChaCha20Poly1305`
73+
74+
Each request starts with a `32-bytes next_strand_key` and a `24-bytes nonce` before the `type` field.
75+
76+
The key is saved on the receiver's end, to be that sender's next `strand_key`. Nonce is also saved as the sender's next `nonce`.
77+
78+
This is a forward, stateful ratchet for the `xChaCha20Poyl1305`. By the rotating the key for every encryption operation, we reduce the "blast radius" in-case a key compormise occurs, then only all data afterwards could be encrypted, until a `PFS` is triggered and new `xChaCha20Poyl1305` strand key is derived from there.
79+
80+
Hiding the `nonces` prevents metadata leakage, and in our scheme it also prevents replay attacks.
81+
Additionally, while all public security proofs of `xChaCha20Poly1305` assume nonce is public, encrypting and or hiding the `nonce` might actually "future-proof" `xChaCha20Poly1305` against potentinal future attacks, leaving only open window through pure dumb brute-forcing of the `32 bytes` key space.
82+
83+
We deliberately avoided using a `KDF`, because this ratchet design is stronger. In `KDF` scheme, an attacker only need to brute-force 1 key to break all previous and future keys. Additionally, keys derived from a single master key suffer from being not true entropy.
84+
85+
This ratchet is not the most optiomal ratchet in the world, but it should be fine, as the primary objective of ``xChaCha20Poly1305` encryption in our protocol, is to simply protect metadata.
86+
87+
Actual "ratchet" and "perfect-forward secrecy" and "post-compromise security" are an inherit part of One-time-pads, which is used to actually encrypt plaintext message contents.
88+
89+
**NOTE**: This ratchet applies to *all* data types, unless expliclity stated otherwise. Receiver, and sender **must** send and save new keys and nonce from / in every request.
90+
6891

6992
#### Human Verification:
7093
`SMP` enforces a human-verifiable question-and-answer process before any chat communication. This prevents "trust on first use"-style attacks that plagues other encrypted protocols.
7194

95+
#### General Acknowledgements:
96+
When you longpoll for new data, each data is appended with a `32 byte` id, once you process all the data, you should send back the acknowledged ids, so that they may be deleted off the server.
97+
98+
This applies to all requests.
99+
72100
### 3. Socialist Millionaire Protocol (`SMP`)
73101
#### 3.1 Initialization (`Alice` -> `Bob`)
74102

@@ -84,27 +112,35 @@ The `payload` is prefixed with `SMP_TYPE = 0x00`.
84112

85113
#### 3.2 Initialization response (`Bob`)
86114

87-
`Bob` generates a shared secret using `Alice’s` `ML-KEM-1024` public key (this is called temporary xchacha key, only used for `SMP` encryption).
115+
`Bob` generates a 4 shared secrets (`128 bytes` total) using `Alice’s` `ML-KEM-1024` public key, and concatenate the ciphertext together:
116+
- 1st key (`0:32` index) is `Bob's` `bob_strand_key`
117+
- 2nd key (`32:64` index) is `Alice's` `alice_strand_key`
118+
- 3rd key (`64:96` index) is `Bob's` `bob_backup_strand_key`
119+
- 4th key (`96:128` index) is `Alice's` `alice_backup_strand_key`.
120+
121+
`Bob` generates two `Strand Nonces` (one for himself, one for `Alice`).
88122

89-
`Bob` generates two `Strand Nonces` (one for himself, one for `Alice`) and hashes each with `SHA3-512`, truncating output to `24 bytes`.
123+
Note: Every key, and nonce, *must* be generated from a cryptographically secure random generator. And all keys and nonces must be `SHA3_512` hashed, then the output must be truncated to the original key or nonce original sizes.
90124

91-
`Bob` also generates an `SMP nonce` for the verification process.
125+
`Bob` also generates an `SMP nonce` for the `SMP` verification process.
92126

93127
`Bob` generates an `ML-DSA-87` key pair for signing (this is called `per-contact signing public key` or just `signing key`).
94128

129+
Note: This key is a long-term key, and **must** be saved, and only used with the contact in question.
130+
95131
`Bob` prepares the `SMP response`:
96132
```
97133
BOB_SIGNING_PUBLIC_KEY || BOB_NONCE || BOB_STRAND_NEXT_NONCE || ALICE_STRAND_NEXT_NONCE
98134
```
99135

100-
`Bob` encrypts the `response` using the derived `temporary XChaCha20 key`.
136+
`Bob` encrypts the `response` using `bob_strand_key` to produce `SMP_RESPONSE_CIPHERTEXTS`.
101137

102138
`Bob` sends:
103139
```
104-
SMP_TYPE || ALICE_ML_KEM_CIPHERTEXT || SMP_RESPONSE_CIPHERTEXT
140+
SMP_TYPE || ALICE_ML_KEM_CIPHERTEXTS || SMP_RESPONSE_CIPHERTEXTS
105141
```
106142

107-
`Bob` saves `Alice` to his contact list locally, however `Bob` **must** flag `Alice` as `unverified` or `pending_verification`.
143+
`Bob` saves `Alice` to his contact list locally, however `Bob` **must** flag `Alice` as `unverified` or `pending_verification` (i.e. do not process any non-SMP requests until the verification succeeds.).
108144
`Bob` also stores all `nonces` and the `temporary XChaCha key` for this `SMP` session.
109145

110146
#### 3.3 Proof 1 (`Alice's` proof of `Bob's` public-key)
@@ -115,7 +151,7 @@ SMP_TYPE || ALICE_ML_KEM_CIPHERTEXT || SMP_RESPONSE_CIPHERTEXT
115151

116152
`Alice` generates her `SMP nonce`.
117153

118-
`Alice` normalizes her `SMP answer` (strip whitespace, lowercase only the first character) and `UTF-8` encodes it.
154+
`Alice` normalizes her `SMP answer` (strip leading and trailing whitespaces, and lowercase *only* the first character) and `UTF-8` encodes it.
119155

120156
Alice generates an `Argon2Id salt` by concatenating `ALICE_SMP_NONCE` to `BOB_SMP_NONCE`, hashes it with `SHA3_512` and truncates back to `16 bytes` (16 bytes for interoperability with libsodium):
121157
```
@@ -148,14 +184,14 @@ Alice generates an `ML-DSA-87` key pair for herself.
148184

149185
`Alice` prepares `SMP` request (Question must be `UTF-8` encoded):
150186
```
151-
SMP_REQUEST_DATA = SMP_TYPE || NEW_ALICE_STRAND_NONCE || ALICE_SIGNING_PUBLIC_KEY || ALICE_SMP_NONCE || ALICE_PROOF_OF_BOB || QUESTION_UTF-8
187+
SMP_REQUEST_DATA = SMP_TYPE || ALICE_SIGNING_PUBLIC_KEY || ALICE_SMP_NONCE || ALICE_PROOF_OF_BOB || QUESTION_UTF-8
152188
```
153189

154-
`Alice` encrypts the payload with `XChaCha20` using `temporary xchacha key` as key, and uses `ALICE_NEXT_STRAND_NONCE` as nonce, and sends it to `Bob`
190+
`Alice` encrypts the payload with the `XChaCha20Poly1305` wrapping scheme sends it to `Bob`.
155191

156192
#### 3.4 Verification & Proof 2 (`Bob`):
157193

158-
`Bob` decrypts the payload with the `temporary key` and asks the user for an `SMP answer`.
194+
`Bob` decrypts the `xChaCha20Poly1305` wrapper, and parses the payload and asks the user for an `SMP answer`.
159195

160196
`Bob` checks if `BOB_SMP_NONCE` is equal to `ALICE_SMP_NONCE`, aborting and sending a `SMP failure request` if they match.
161197

@@ -166,7 +202,7 @@ If verification *fails*:
166202
```
167203
SMP_REQUEST_DATA = SMP_TYPE || b"failure".
168204
```
169-
`Bob` encrypts the payload using `temporary chacha key`, but does not use his **Strand Nonce**, instead he generates a random nonce and bundles it at start of the **ciphertext**.
205+
`Bob` encrypts the payload with the `XChaCha20Poly1305` wrapping scheme sends it to `Alice
170206

171207
If verification *succeeds*:
172208

@@ -189,36 +225,37 @@ PROOF_DATA = ALICE_SMP_NONCE || BOB_SMP_NONCE || ALICE_FINGERPRINT
189225
SMP_REQUEST_DATA = SMP_TYPE || BOB_NEW_STRAND_NONCE || BOB_PROOF_OF_ALICE || BOB_STRAND_KEY || ALICE_STRAND_KEY
190226
```
191227

192-
`Bob` encrypts the `SMP request data` with `temporary xchacha key` as key, and previous `BOB_NEXT_STRAND_NONCE` as nonce, and sends to `Alice`.
228+
`Bob` encrypts the `SMP request data` with the `XChaCha20Poly1305` wrapping scheme sends it to `Alice`
193229

194-
`Bob` modifies both `ALICE_STRAND_KEY` and `BOB_STRAND_KEY` by `XOR-ing` each key with the `SHA3-512` hash of `ANSWER_SECRET`:
230+
`Bob` modifies both the ratchet's new `ALICE_STRAND_KEY` and `BOB_STRAND_KEY` by `XOR-ing` each key with the `SHA3-512` hash of `ANSWER_SECRET` and truncating the hash output back to `32 bytes`:
195231
```
196-
ALICE_STRAND_KEY = XOR(ALICE_STRAND_KEY, SHA3_512(ANSWER_SECRET))
197-
BOB_STRAND_KEY = XOR(BOB_STRAND_KEY, SHA3_512(ANSWER_SECRET))
232+
ALICE_NEW_STRAND_KEY = XOR(SHA3_512(ANSWER_SECRET)[:32], ALICE_NEW_STRAND_KEY)
233+
BOB_NEW_STRAND_KEY = XOR(SHA3_512(ANSWER_SECRET)[:32], BOB_NEW_STRAND_KEY)
198234
```
199235

236+
`Bob` then saves the new keys as always (in all throughout the protocol, we implicility imply you must save new ratchet keys, but in this specific stage, we add a special transformation before saving them).
200237

201-
`Bob` then saves the new keys, and marks `Alice` as `SMP` verified.
238+
`Bob` then marks `Alice` as `SMP` "verified".
202239

203240
#### 3.5 Final Verification (`Alice`):
204241

205242
`Alice` decrypts `Bob’s` `SMP` payload and verifies `Bob’s` proof.
206243

207-
If valid, she applies the same `XOR` transformation to the `Strand Keys`, and saves them.
244+
If valid, she applies the same `XOR` transformation to the `Next Strand Keys`, and saves them.
208245

209246
`Alice` marks `Bob` as verified.
210247

211-
`Alice` sends her first PFS keys.
248+
`Alice` sends her first `PFS` keys.
212249

213250
#### 3.6. Notes on SMP:
214251

215252
Step 1: No encryption
216253

217-
Step 2: Encryption is being set up
254+
Step 2: `xChaCha20Poly1305` ratchet encryption is being set up
218255

219-
Step 3 and onwards: All requests are encrypted with the `temporary xchacha key` and `nonces` protected using the `strand nonces` by bundling next nonce to be used in every request.
256+
Step 3 and onwards: All requests are encrypted with `xChaCha20Poly1305` wrapping, by bundling next nonce and key to be used in every request. This applies not just to `SMP`, but to all other steps.
220257

221-
Nonces are embedded in payloads, not sent in clear, except in step 2 and SMP failure requests
258+
Nonces are embedded in payloads, not sent in clear, except in step 2.
222259

223260
Do not confuse `Strand Nonces` with `SMP Nonces`, the latter is only used for SMP process (as salt for `Argon2id`, etc, not for encryption), while the former is used in Step 3 and onwards, even in other requests types (`PFS`, `MSGS`, etc.)
224261

@@ -231,7 +268,7 @@ We highly recommend implementations to only allow user to set a `8+ character` a
231268

232269
Even though the `question` is encrypted, an active *Man-in-the-middle* adversary **can still retrieve it**. The verification would fail, but the adversary would have the `question` plaintext.
233270

234-
This is acceptable, as the purpose of encrypting `SMP` process is to hide *metadata* against **passive** adversaries, not an **active** adversary.
271+
This is acceptable, as the purpose of encrypting the `SMP` process is to hide *metadata* against **passive** adversaries, not an **active** adversary.
235272

236273
The question **must not** contain any senstive data. And it must not contain any hints to the answer.
237274

@@ -258,12 +295,12 @@ hash_chain || ml_kem_1024_public_key || optional_classic_mceliece_8192128_public
258295

259296
`Alice` generates `ALICE_NEW_STRAND_NONCE`.
260297

261-
`Alice` constructs `PFS` request:
298+
`Alice` constructs `PFS` request (`PFS_NEW` = `\x01`):
262299
```
263-
PFS_PAYLOAD = PFS_TYPE || ALICE_NEW_STRAND_NONCE || PUBLICKEYS_HASHCHAIN_SIGNATURE || PUBLICKEYS_HASHCHAIN
300+
PFS_PAYLOAD = PFS_NEW || ALICE_NEW_STRAND_NONCE || PUBLICKEYS_HASHCHAIN_SIGNATURE || PUBLICKEYS_HASHCHAIN
264301
```
265302

266-
`Alice` then encrypts the `PFS_PAYLOAD`, with `xChaCha20Poly1305` using her `ALICE_STRAND_KEY` key, and using previous `ALICE_NEXT_STRAND_NONCE` as nonce.
303+
`Alice` does not use the new keys, but saves them until she receives an `PFS ack` request, which she then deletes old keys, and uses the new keys.
267304

268305
#### 4.2 Receiving PFS Keys (`Bob`)
269306

@@ -275,6 +312,11 @@ PFS_PAYLOAD = PFS_TYPE || ALICE_NEW_STRAND_NONCE || PUBLICKEYS_HASHCHAIN_SIGNATU
275312

276313
`Bob` determines which keys were sent (`ML-KEM-1024` only or `ML-KEM-1024` + `Classic-McEliece-8192128`), and saves them.
277314

315+
Afterwards, `Bob` then sends an `PFS ack` (`PFS_ACK` = `\x02`) request to `Alice`, informing her that he received keys:
316+
```
317+
PFS_ACK_PAYLOAD = PFS_ACK
318+
```
319+
278320
`Bob` then checks if he already sent keys to `Alice`, if he never sent any keys before to `Alice`, he performs the `4.1. Key Rotation` as well.
279321

280322
#### 4.3. PFS Notes:
@@ -283,6 +325,9 @@ Even though the use of hash-chains and signatures may appear redundant here, as
283325
The reason we opted for a hash-chain based design, instead of a simple counter, is to ensure metadata of how many key rotations occured never gets leaked, even when `xChaCha20Poly1305` is broken.
284326
Even if `Alice's` or `Bob's` endpoint get compromised, no metadata of how many key rotation occured could be recovered.
285327

328+
Acknowlegement make this design fully async, and arguably more secure than other async `PFS` schemes, as only one set of keypairs are at use at any time.
329+
330+
286331
### 5. Messaging (`MSGS`)
287332
#### 5.1 OTP Batch Generation (`Alice`)
288333
`Alice` uses `Bob's` `ML-KEM-1024` public-key to generates many shared secrets *in chunks*, concatenating them until their total size reaches (or exceeds) `OTP_PAD_SIZE` size (default `11264 bytes`).

0 commit comments

Comments
 (0)