|
| 1 | +# Strandlock Protocol Specification |
| 2 | +Version: `1.0` |
| 3 | +Date: `2025-09-08` |
| 4 | +Author(s): `ChadSec1` (`Freedom Club Sec`) |
| 5 | +Contact: github.com/Freedom-Club-Sec |
| 6 | +Status: `Draft` |
| 7 | +Intended Audience: Security engineers, cryptographers, protocol implementers |
| 8 | +### 0. Overview |
| 9 | + |
| 10 | +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. |
| 11 | + |
| 12 | +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. |
| 13 | +If `xChaCha20poly1305` is broken, messages remain safe as long as (at least) one `KEM` is uncompromised. |
| 14 | +If `OTP` implementation has mistakes, messages remain safe as long as `xChaCha20Poly1305` is remains unbroken. |
| 15 | + |
| 16 | +If Both `KEMs`, and `xChaCha20Poly1305` are compromised in future, as long as `OTP batch` request was not intercepted nor logged, messages remain safe. |
| 17 | + |
| 18 | +*`Strandlock`* is transport-agnostic. It can operate over any underlying protocol, including federated chat systems like *`Coldwire`* or raw `TCP` sockets. |
| 19 | + |
| 20 | +### 1. Terminology |
| 21 | + |
| 22 | +For clarity, the following terms are used consistently throughout this specification: |
| 23 | + |
| 24 | +##### Request |
| 25 | +A generic protocol message sent from one party to another. A Request is not bound to `HTTP` or any specific transport mechanism; it may be carried over `TCP`, `UDP`, `sockets`, `pipes`, or any `communication medium`. |
| 26 | + |
| 27 | +##### Response |
| 28 | +The reply to a Request, carrying the necessary protocol data to complete or continue a Strandlock operation. |
| 29 | +We use term "Response" and "Request" interchangably. A response is a request. |
| 30 | + |
| 31 | +##### Session |
| 32 | +A logical state maintained between two parties that tracks shared secrets, nonces, and protocol progress. A session may span multiple Requests and Responses. Only one session is allowed per-contact. This protocol does not (and will not) support multi-devices, nor multi-session per same contact. |
| 33 | + |
| 34 | +##### Strand Nonce |
| 35 | +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. |
| 36 | + |
| 37 | +##### Strand Key |
| 38 | +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" |
| 39 | + |
| 40 | + |
| 41 | +##### SMP (Socialist Millionaire Protocol) |
| 42 | +An authentication mechanism used to confirm shared knowledge between two parties without revealing the secret itself. |
| 43 | +It does not refer to vanilla SMP, but refers to Off-the-record messaging-style `SMP`. |
| 44 | + |
| 45 | +##### OTP Batch |
| 46 | +A collection of one-time pad material derived from multiple primitives and signed before being used for message encryption. |
| 47 | + |
| 48 | +### 2. General Protocol Features |
| 49 | + |
| 50 | +##### Request Types: |
| 51 | +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. |
| 52 | + |
| 53 | +##### Nonces: |
| 54 | +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. |
| 55 | +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. |
| 56 | + |
| 57 | +##### Encryption: |
| 58 | +All payloads are encrypted with `XChaCha20Poly1305`, except for the `SMP` initiation stage. |
| 59 | + |
| 60 | +##### Human Verification: |
| 61 | +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. |
| 62 | + |
| 63 | +### 3. SMP (Socialist Millionaire Protocol) |
| 64 | +##### 3.1 Initialization (Alice -> Bob) |
| 65 | + |
| 66 | +`Alice` selects an `SMP` question and answer and stores them locally. |
| 67 | + |
| 68 | +`Alice` generates an `ML-KEM-1024` key pair for Bob and saves the keys locally. |
| 69 | + |
| 70 | +`Alice` sends the following to the server: |
| 71 | +- Metadata: recipient (contact address) |
| 72 | +- Blob: `Alice’s` `ML-KEM-1024` public key |
| 73 | + |
| 74 | +The `payload` is prefixed with `SMP_TYPE = 0x00`. |
| 75 | + |
| 76 | +##### 3.2 Response (Bob -> Alice) |
| 77 | + |
| 78 | +`Bob` generates a shared secret using `Alice’s` `ML-KEM-1024` public key (this is called temporary xchacha key, only used for `SMP` encryption). |
| 79 | + |
| 80 | +`Bob` generates two `Strand Nonces` (one for himself, one for `Alice`) and hashes each with `SHA3-512`, truncating output to `24 bytes`. |
| 81 | + |
| 82 | +`Bob` also generates an `SMP nonce` for the verification process. |
| 83 | + |
| 84 | +`Bob` generates an `ML-DSA-87` key pair for signing (this is called `per-contact signing public key` or just `signing key`). |
| 85 | + |
| 86 | +`Bob` prepares the `SMP response`: |
| 87 | +``` |
| 88 | +BOB_SIGNING_PUBLIC_KEY || BOB_NONCE || BOB_STRAND_NEXT_NONCE || ALICE_STRAND_NEXT_NONCE |
| 89 | +``` |
| 90 | + |
| 91 | +`Bob` encrypts the `response` using the derived `temporary XChaCha20 key`. |
| 92 | + |
| 93 | +`Bob` sends: |
| 94 | +``` |
| 95 | +SMP_TYPE || ALICE_ML_KEM_CIPHERTEXT || SMP_RESPONSE_CIPHERTEXT |
| 96 | +``` |
| 97 | + |
| 98 | +`Bob` saves `Alice` to his contact list locally, however `Bob` **must** flag `Alice` as `unverified` or `pending_verification`. |
| 99 | +`Bob` also stores all `nonces` and the `temporary XChaCha key` for this `SMP` session. |
| 100 | + |
| 101 | +##### 3.3 Proof 1 (`Alice's` proof of `Bob's` public-key) |
| 102 | + |
| 103 | +`Alice` decapsulates the KEM ciphertext, derives shared secret to get the "temporary xChaCha key", then she decrypts the `XChaCha` ciphertext using the `temporary xChaCha key`. |
| 104 | + |
| 105 | +`Alice` stores `Bob’s` signing key, `Bob's SMP nonce`, `Alice Strand Nonce`, and `Bob Strand Nonce` . |
| 106 | + |
| 107 | +`Alice` generates her `SMP nonce`. |
| 108 | + |
| 109 | +`Alice` normalizes her `SMP answer` (strip whitespace, lowercase only the first character) and `UTF-8` encodes it. |
| 110 | + |
| 111 | +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): |
| 112 | +``` |
| 113 | +ARGON2ID_SALT = SHA3-512(BOB_SMP_NONCE || ALICE_SMP_NONCE)[:16] |
| 114 | +``` |
| 115 | + |
| 116 | +`Alice` derives `answer_secret` using `Argon2id` with the `salt`, and the following paramaters: |
| 117 | +- Memory: 3072 * 1024 bytes (3 Gigabytes) |
| 118 | +- Iterations: 50000 (50k) |
| 119 | +- Output: 64 bytes |
| 120 | + |
| 121 | +`Alice` computes `Bob’s` public-key fingerprint: |
| 122 | +``` |
| 123 | +BOB_FINGERPRINT = SHA3-512(BOB_SIGNING_PUBLIC_KEY) |
| 124 | +``` |
| 125 | + |
| 126 | +`Alice` prepares `proof` data: |
| 127 | +``` |
| 128 | +PROOF_DATA = BOB_SMP_NONCE || ALICE_SMP_NONCE || BOB_KEY_FINGERPRINT |
| 129 | +``` |
| 130 | + |
| 131 | +`Alice` computes the proof by performing HMAC operation on `PROOF_DATA` with key being `ANSWER_SECRET` and algorithm being `SHA3_512`: |
| 132 | +``` |
| 133 | +PROOF = HMAC(SHA3_512, PROOF_DATA, ANSWER_SECRET) |
| 134 | +``` |
| 135 | + |
| 136 | +`Alice` generates a `New Strand Nonce` (random `24 bytes`, hashed with `SHA3-512`, truncated to `24 bytes`). |
| 137 | + |
| 138 | +Alice generates an `ML-DSA-87` key pair for herself. |
| 139 | + |
| 140 | +`Alice` prepares `SMP` request (Question must be `UTF-8` encoded): |
| 141 | +``` |
| 142 | +SMP_REQUEST_DATA = SMP_TYPE || NEW_ALICE_STRAND_NONCE || ALICE_SIGNING_PUBLIC_KEY || ALICE_SMP_NONCE || ALICE_PROOF_OF_BOB || QUESTION_UTF-8 |
| 143 | +``` |
| 144 | + |
| 145 | +`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` |
| 146 | + |
| 147 | +##### 3.4 Verification & Proof 2 (`Bob`): |
| 148 | + |
| 149 | +`Bob` decrypts the payload with the `temporary key` and asks the user for an `SMP answer`. |
| 150 | + |
| 151 | +`Bob` checks if `BOB_SMP_NONCE` is equal to `ALICE_SMP_NONCE`, aborting and sending a `SMP failure request` if they match. |
| 152 | + |
| 153 | +`Bob` verifies `Alice’s` `proof` **using a time-constant comparison**. |
| 154 | + |
| 155 | +If verification *fails*: |
| 156 | +`Bob` prepares `SMP failure request` payload data: |
| 157 | +``` |
| 158 | +SMP_REQUEST_DATA = SMP_TYPE || b"failure". |
| 159 | +``` |
| 160 | +`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**. |
| 161 | + |
| 162 | +If verification *succeeds*: |
| 163 | + |
| 164 | +`Bob` computes the `fingerprint` of `Alice’s` `KEM` and `signing` public-keys: |
| 165 | +``` |
| 166 | +ALICE_FINGERPRINT = SHA3-512(ALICE_SIGNING_PUBLIC_KEY || ALICE_KEM_PUBLIC_KEY) |
| 167 | +``` |
| 168 | + |
| 169 | +`Bob` prepares proof data: |
| 170 | +``` |
| 171 | +PROOF_DATA = ALICE_SMP_NONCE || BOB_SMP_NONCE || ALICE_FINGERPRINT |
| 172 | +``` |
| 173 | + |
| 174 | +`Bob` generates `ALICE_STRAND_KEY` and `BOB_STRAND_KEY`, which will be used for all other `non-SMP requests` going forward. (`32 bytes` each, random bytes, `SHA3-512` hashed, then truncated back to `32 bytes`) |
| 175 | + |
| 176 | +`Bob` generates a new `BOB_NEW_STRAND_NONCE` |
| 177 | + |
| 178 | +`Bob` prepares `SMP` payload data: |
| 179 | +``` |
| 180 | +SMP_REQUEST_DATA = SMP_TYPE || BOB_NEW_STRAND_NONCE || BOB_PROOF_OF_ALICE || BOB_STRAND_KEY || ALICE_STRAND_KEY |
| 181 | +``` |
| 182 | + |
| 183 | +`Bob` encrypts the `SMP request data` with `temporary xchacha key` as key, and previous `BOB_NEXT_STRAND_NONCE` as nonce, and sends to `Alice`. |
| 184 | + |
| 185 | +`Bob` modifies both `ALICE_STRAND_KEY` and `BOB_STRAND_KEY` by `XOR-ing` each key with the `SHA3-512` hash of `ANSWER_SECRET`: |
| 186 | +``` |
| 187 | +
|
| 188 | +ALICE_STRAND_KEY = XOR(ALICE_STRAND_KEY, SHA3_512(ANSWER_SECRET)) |
| 189 | +BOB_STRAND_KEY = XOR(BOB_STRAND_KEY, SHA3_512(ANSWER_SECRET)) |
| 190 | +``` |
| 191 | +`Bob` then saves the new keys, and marks `Alice` as `SMP` verified. |
| 192 | + |
| 193 | +3.5 Final Verification (`Alice`): |
| 194 | + |
| 195 | +`Alice` decrypts `Bob’s` `SMP` payload and verifies `Bob’s` proof. |
| 196 | + |
| 197 | +If valid, she applies the same XOR transformation to the `Strand Keys`, and saves them. |
| 198 | + |
| 199 | +`Alice` marks `Bob` as verified. |
| 200 | + |
| 201 | +`Alice` sends her first PFS keys. |
| 202 | + |
| 203 | +##### 3.6. Notes on SMP: |
| 204 | + |
| 205 | +Step 1: No encryption |
| 206 | + |
| 207 | +Step 2: Encryption is being set up |
| 208 | + |
| 209 | +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. |
| 210 | + |
| 211 | +Nonces are embedded in payloads, not sent in clear, except in step 2 and SMP failure requests |
| 212 | + |
| 213 | +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.) |
| 214 | + |
| 215 | +The security of the `SMP` process depends entirely on the entropy of the user-provided `answer`, we use extreme `Argon2id` parameters to protect against a *"god-like"* adversary with virtually *unlimited* computing power, and we salt the answer to prevent *Rainbow-style* attacks. **However**, if `answer` is *low-entropy*, even such measures cannot completely prevent the cracking of the answer. |
| 216 | +We highly recommend implementations to only allow user to set a `8+ character` answer, and to check the entropy of provided answer (is all lowercase, is all uppercase, is only digits, etc), and to warn (or prevent) the user from continuing. |
| 217 | + |
| 218 | +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. |
| 219 | +This is acceptable, as the purpose of encrypting `SMP` process is to hide *metadata* against **passive** adversaries, not an **active** adversary. |
| 220 | +The question **must not** contain any senstive data. And it must not contain any hints to the answer. |
| 221 | +Implementations **must** check `answer` and `question` in initation stage, to ensure neither contain the other. |
| 222 | + |
| 223 | + |
| 224 | +### 4. Perfect Forward Secrecy (PFS) |
| 225 | +##### 4.1 Key Rotation |
| 226 | + |
| 227 | +Alice checks if a saved ALICE_KEYS_HASH_CHAIN exists: |
| 228 | + |
| 229 | +If not, she generates a new hash chain of size KEYS_HASH_CHAIN_LEN. |
| 230 | + |
| 231 | +Otherwise, she advances the hash chain using SHA3-512. |
| 232 | + |
| 233 | +Alice generates new ML-KEM-1024 key pairs. |
| 234 | + |
| 235 | +Alice checks if McEliece keys need rotation (after 10 OTP batches or if never sent before). |
| 236 | + |
| 237 | +Alice constructs publickeys_hashchain: |
| 238 | + |
| 239 | +hash_chain || ml_kem_1024_public_key || optional_classic_mceliece_8192128f_public_key |
| 240 | + |
| 241 | +Alice signs publickeys_hashchain with her signing key. |
| 242 | + |
| 243 | +Alice generates ALICE_NEW_STRAND_NONCE. |
| 244 | + |
| 245 | +Alice sends: |
| 246 | + |
| 247 | +PFS_TYPE || ALICE_NEW_STRAND_NONCE || PUBLICKEYS_HASHCHAIN_SIGNATURE || PUBLICKEYS_HASHCHAIN |
| 248 | + |
| 249 | + |
| 250 | +Encrypted using previous ALICE_NEXT_STRAND_NONCE. |
| 251 | + |
| 252 | +4.2 Receiving PFS Keys (Bob) |
| 253 | + |
| 254 | +Bob decrypts using ALICE_STRAND_KEY and ALICE_NEXT_STRAND_NONCE. |
| 255 | + |
| 256 | +Bob updates ALICE_NEXT_STRAND_NONCE. |
| 257 | + |
| 258 | +Bob verifies hash chain and signature. |
| 259 | + |
| 260 | +Bob determines which keys were sent (ML-KEM-1024 only or ML-KEM + McEliece) and saves them. |
| 261 | + |
| 262 | +If Bob has no new keys to send, he generates them similarly. |
| 263 | + |
| 264 | +5. Messaging (MSGS) |
| 265 | +5.1 OTP Batch Generation |
| 266 | + |
| 267 | +Alice checks if message length + OTP_SIZE_LENGTH ≤ available pad space: |
| 268 | + |
| 269 | +If not, she generates a new OTP batch. |
| 270 | + |
| 271 | +Alice generates ML-KEM-1024 shared secrets in chunks until OTP_PAD_SIZE is reached. |
| 272 | + |
| 273 | +Alice generates Classic McEliece shared secrets similarly. |
| 274 | + |
| 275 | +Alice generates OTP_PAD_SIZE of random bytes for XChaCha shared secrets. |
| 276 | + |
| 277 | +Alice generates a new hash chain seed MESSAGE_HASH_CHAIN_LEN (64 bytes). |
| 278 | + |
| 279 | +Alice signs all ciphertexts using her signing key. |
| 280 | + |
| 281 | +Alice generates ALICE_NEW_STRAND_NONCE and prepares the payload: |
| 282 | + |
| 283 | +MSG_TYPE || 0x00 || ALICE_NEW_STRAND_NONCE || HASH_CHAIN_SEED || OTP_BATCH_SIGNATURE || ML_KEM_1024_CIPHERTEXT || CLASSIC_MCELIESE_819_CIPHERTEXT || XCHACHA_SHARED_SECRETS |
| 284 | + |
| 285 | + |
| 286 | +Alice encrypts using ALICE_STRAND_KEY and sends. |
| 287 | + |
| 288 | +Alice XORs ML-KEM, McEliece, and XChaCha secrets to produce OTP pads. |
| 289 | + |
| 290 | +The first 32 bytes of pads become the new ALICE_STRAND_KEY. |
| 291 | + |
| 292 | +5.2 Message Sending |
| 293 | + |
| 294 | +Alice UTF-8 encodes the message. |
| 295 | + |
| 296 | +Alice OTP encrypts the message with generated pads: |
| 297 | + |
| 298 | +If length < OTP_MAX_BUCKET - OTP_SIZE_LENGTH, pad to OTP_MAX_BUCKET. |
| 299 | + |
| 300 | +If length > OTP_MAX_BUCKET, pad randomly up to OTP_MAX_RANDOM_PAD. |
| 301 | + |
| 302 | +Prefix message with padding length (OTP_SIZE_LENGTH, 2 bytes, big-endian). |
| 303 | + |
| 304 | +Advance hash chain: SHA3-512(previous_hash_chain || encrypted_message) |
| 305 | + |
| 306 | +Generate ALICE_NEW_STRAND_NONCE. |
| 307 | + |
| 308 | +Prepare payload: |
| 309 | + |
| 310 | +MSG_TYPE || 0x01 || ALICE_NEW_STRAND_NONCE || HASH_CHAIN || MESSAGE_ENCRYPTED |
| 311 | + |
| 312 | + |
| 313 | +Encrypt with ALICE_STRAND_KEY using ALICE_NEXT_STRAND_NONCE and send. |
| 314 | + |
| 315 | +5.3 Receiving Messages (Bob) |
| 316 | + |
| 317 | +Decrypt payload using ALICE_STRAND_KEY and nonce. |
| 318 | + |
| 319 | +Verify hash chain. |
| 320 | + |
| 321 | +Decrypt message with OTP pads from OTP batch. |
| 322 | + |
| 323 | +Display message. |
| 324 | + |
| 325 | +6. Argon2id Parameters for SMP |
| 326 | + |
| 327 | +Memory: 3GB (3072 * 1024 KB) |
| 328 | + |
| 329 | +Iterations: 50,000 |
| 330 | + |
| 331 | +Output: 64 bytes |
| 332 | + |
| 333 | +7. Design Notes |
| 334 | + |
| 335 | +Nonce hiding: Prevents metadata leakage and hides rare nonce collisions. |
| 336 | + |
| 337 | +OTP usage: Provides additional protection even if XChaCha is broken. Makes known-plaintext attacks ineffective. |
| 338 | + |
| 339 | +Composite security: Messages remain secure if an attacker breaks a single primitive; at least 3 primitives need to be broken for full compromise (ML-KEM, McEliece, XChaCha or SMP). |
| 340 | + |
| 341 | +xChaCha20-Poly1305 vs AES-GCM: Larger nonce, no hardware dependencies, reduces potential backdoors. |
| 342 | + |
| 343 | +SHA3-512 whitening: Reduces CSPRNG bias and protects against metadata leakage. |
| 344 | + |
| 345 | +Argon2id for SMP: Protects against brute force attacks and Trust-On-First-Use style attacks. |
0 commit comments