<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: secret_KK</title>
    <description>The latest articles on DEV Community by secret_KK (@secret_kk).</description>
    <link>https://dev.to/secret_kk</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3916258%2F5ee74c77-0bab-4295-825a-743291a11bb8.png</url>
      <title>DEV Community: secret_KK</title>
      <link>https://dev.to/secret_kk</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/secret_kk"/>
    <language>en</language>
    <item>
      <title>Designing an E2E-Encrypted Terminal Chat in C++17: SRP-6a, HKDF, and a Relay-Blind Server</title>
      <dc:creator>secret_KK</dc:creator>
      <pubDate>Wed, 06 May 2026 15:15:51 +0000</pubDate>
      <link>https://dev.to/secret_kk/i-built-an-end-to-end-encrypted-command-line-chat-in-c17-srp-6a-fernet-hkdf-5h50</link>
      <guid>https://dev.to/secret_kk/i-built-an-end-to-end-encrypted-command-line-chat-in-c17-srp-6a-fernet-hkdf-5h50</guid>
      <description>&lt;p&gt;There is a class of security properties that most hobby chat implementations simply skip: the server should not be able to read your messages, and authentication should not require trusting the server with a password hash. &lt;code&gt;cmd_chat&lt;/code&gt; is a deliberately minimal C++17 implementation that takes both of these seriously — using &lt;strong&gt;SRP-6a&lt;/strong&gt;, &lt;strong&gt;HKDF-SHA256&lt;/strong&gt;, and a &lt;strong&gt;Fernet-compatible AEAD&lt;/strong&gt; scheme — without hiding the mechanics behind a TLS library.&lt;/p&gt;

&lt;p&gt;This post is about the design decisions, the trade-offs, and the places where I deliberately kept the implementation simple in ways that a production system would not.&lt;/p&gt;




&lt;h2&gt;
  
  
  Design Goals and Non-Goals
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Goals:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The server relays ciphertext it cannot decrypt. No key material touches the server after authentication.&lt;/li&gt;
&lt;li&gt;Authentication is mutual and zero-knowledge. Neither side learns the other's secret; both sides prove they share it.&lt;/li&gt;
&lt;li&gt;The crypto stack is auditable in a single afternoon. No opaque abstractions.&lt;/li&gt;
&lt;li&gt;Cross-platform: Windows (Winsock2), Linux, macOS — same source tree, CI on all three.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Non-goals (explicitly deferred):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Perfect forward secrecy. All session confidentiality is tied to the room password. An ephemeral ECDH layer per connection would fix this; it is on the roadmap.&lt;/li&gt;
&lt;li&gt;TLS transport. The current framing is JSON lines over raw TCP — entirely appropriate for a trusted network or a demo, not appropriate for public deployment without wrapping in TLS.&lt;/li&gt;
&lt;li&gt;Persistent storage. The message store is in-memory. Replacing it with SQLite is a small mechanical change that would add no insight here.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  System Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cmd_chat_cpp/
├── CMakeLists.txt
├── client/          ← TCP connect, SRP handshake, send/recv threads, UI
├── server/          ← accept loop, per-client threads, in-memory stores, broadcast
└── common/          ← crypto.hpp, base64.hpp, json_io.hpp, uuid.hpp  (header-only)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Transport:&lt;/strong&gt; Newline-delimited JSON (NDJSON) over raw TCP. One &lt;code&gt;nlohmann::json&lt;/code&gt; object per line. The framing is intentionally simple — any JSON parser and &lt;code&gt;nc&lt;/code&gt; can participate, which matters for debugging and future language interoperability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Server threading model:&lt;/strong&gt; One &lt;code&gt;std::thread&lt;/code&gt; per accepted connection, detached. Shared state (&lt;code&gt;MessageStore&lt;/code&gt;, &lt;code&gt;UserSessionStore&lt;/code&gt;, &lt;code&gt;ConnectionManager&lt;/code&gt;, &lt;code&gt;SRPAuthManager&lt;/code&gt;) is guarded by per-object mutexes. This is the classic thread-per-connection model: straightforward to reason about, does not scale to thousands of concurrent clients, and is entirely appropriate for the stated scope.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Client threading model:&lt;/strong&gt; Main thread owns stdin and the send path. A dedicated receive thread runs &lt;code&gt;recv_loop()&lt;/code&gt; and updates the display. The two threads share no mutable state beyond the socket handle, which is safe after connection establishment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Security Design
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client                               Server
  |                                    |
  |--- SRP step 1: username + A ------&amp;gt;|
  |&amp;lt;-- SRP step 2: salt + B -----------|
  |--- SRP step 3: client proof M ----&amp;gt;|
  |&amp;lt;-- SRP step 4: server proof H_AMK -|
  |                                    |
  |  (both sides now hold session_key) |
  |                                    |
  |  room_key = HKDF(session_key,      |
  |                  room_salt,        |
  |                  "room_key")       |
  |                                    |
  |--- Fernet(room_key, plaintext) ---&amp;gt;|  ← server sees only opaque base64
  |&amp;lt;-- Fernet(room_key, plaintext) ----|
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Layer 1: SRP-6a — Password-Authenticated Key Exchange
&lt;/h3&gt;

&lt;p&gt;SRP is a PAKE: it gives you mutual authentication and a shared session key, and the wire messages are computationally indistinguishable from random to a passive observer who does not know the password. The server stores a verifier &lt;code&gt;v = g^x mod N&lt;/code&gt; (where &lt;code&gt;x = H(salt | password)&lt;/code&gt;), never the password itself.&lt;/p&gt;

&lt;p&gt;The handshake produces &lt;code&gt;session_key = H(A | B | S)&lt;/code&gt; independently on both sides, where &lt;code&gt;S&lt;/code&gt; is the shared premaster secret. Neither &lt;code&gt;x&lt;/code&gt; nor &lt;code&gt;S&lt;/code&gt; is ever transmitted.&lt;/p&gt;

&lt;p&gt;I used &lt;a href="https://github.com/cocagne/csrp" rel="noopener noreferrer"&gt;csrp&lt;/a&gt; with &lt;code&gt;SRP_NG_2048&lt;/code&gt; and &lt;code&gt;SRP_SHA256&lt;/code&gt;. The server creates the verifier once at startup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;srp_create_salted_verification_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;SRP_SHA256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SRP_NG_2048&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"chat"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;reinterpret_cast&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;*&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;bytes_s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;len_s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;bytes_v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;len_v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;nullptr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;nullptr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One deliberate simplification: all clients authenticate as the identity &lt;code&gt;"chat"&lt;/code&gt;. The username is a display name only, not a separate credential. This means the SRP verifier is shared across all clients — the password is the room credential, not a per-user one. That is a group chat model, not a user account model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 2: HKDF-SHA256 — Room Key Derivation
&lt;/h3&gt;

&lt;p&gt;After SRP, every client that authenticated with the same password holds the same &lt;code&gt;session_key&lt;/code&gt;. HKDF turns that into a deterministic, domain-separated encryption key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;room_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;HKDF&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nc"&gt;SHA256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ikm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;session_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;salt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;room_salt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;room_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;room_salt&lt;/code&gt; is 16 bytes of &lt;code&gt;RAND_bytes&lt;/code&gt; generated at server startup and transmitted during the auth handshake. The &lt;code&gt;info&lt;/code&gt; parameter provides domain separation — if you later derive a MAC key or a different-purpose key from the same IKM, use a different &lt;code&gt;info&lt;/code&gt; string and you get an independent key with no relation to &lt;code&gt;room_key&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The implementation in &lt;code&gt;common/crypto.hpp&lt;/code&gt; covers both the OpenSSL 3 &lt;code&gt;EVP_KDF&lt;/code&gt; API and the legacy &lt;code&gt;EVP_PKEY_derive&lt;/code&gt; path, since both are in active use in the wild:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;hkdf_sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;ikm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;out_len&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Layer 3: Fernet-Compatible AEAD — Message Encryption
&lt;/h3&gt;

&lt;p&gt;Each message is encrypted into a Fernet token. The layout is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;token = version(1B 0x80) | timestamp(8B big-endian) | IV(16B) | ciphertext | HMAC-SHA256(32B)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The HMAC covers everything from &lt;code&gt;version&lt;/code&gt; through the end of &lt;code&gt;ciphertext&lt;/code&gt;. This is encrypt-then-MAC — the MAC is over the ciphertext, not the plaintext, which is what you want for a padding-oracle-resistant scheme. AES-128-CBC with a fresh &lt;code&gt;RAND_bytes&lt;/code&gt; IV per message.&lt;/p&gt;

&lt;p&gt;The server stores and rebroadcasts the base64-encoded token unchanged. It has no key. It cannot decrypt, forge, or modify a message without the HMAC check failing on the receiving client.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;fernet_encrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;plaintext&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;fernet_decrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One honest note: AES-128-CBC is not the current best practice for new designs — AES-256-GCM gives you authenticated encryption natively, without a separate HMAC, and eliminates the IV-reuse-is-catastrophic property of CBC. I used CBC + HMAC to match the Fernet specification precisely and to keep the construction transparent. For a production system, reach for GCM or ChaCha20-Poly1305.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Wire Protocol
&lt;/h2&gt;

&lt;p&gt;Authentication phase (four round trips):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"cmd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"srp_init"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"alice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"A"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;base64&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;uuid&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"B"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;base64&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"salt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;hex&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"room_salt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;base64&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"cmd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"srp_verify"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;uuid&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"M"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;base64&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"H_AMK"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;base64&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"session_key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;base64&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chat phase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"init"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nl"&gt;"messages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"users"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"alice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bob"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;fernet-token&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"alice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;fernet-token&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user_joined"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bob"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user_left"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nl"&gt;"username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bob"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;json_io.hpp&lt;/code&gt; helpers handle framing — send appends &lt;code&gt;\n&lt;/code&gt;, recv reads until &lt;code&gt;\n&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;send_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SOCKET&lt;/span&gt; &lt;span class="n"&gt;sock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;nlohmann&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;optional&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;nlohmann&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;recv_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SOCKET&lt;/span&gt; &lt;span class="n"&gt;sock&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;std::optional&lt;/code&gt; on &lt;code&gt;recv_json&lt;/code&gt; is deliberate — &lt;code&gt;nullopt&lt;/code&gt; signals EOF or a parse error, which the caller uses to terminate the connection cleanly rather than entering an invalid state.&lt;/p&gt;




&lt;h2&gt;
  
  
  Known Limitations Worth Naming
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No forward secrecy.&lt;/strong&gt; If the room password is ever compromised, all past session keys (which are derived from the password via SRP) can be recomputed and all past messages can be decrypted — assuming an adversary recorded the traffic. An ephemeral ECDH exchange per connection would bound the blast radius to that session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Replay is possible at the SRP layer.&lt;/strong&gt; The current implementation does not validate that the SRP ephemeral values &lt;code&gt;A&lt;/code&gt; and &lt;code&gt;B&lt;/code&gt; are fresh across reconnections. A full implementation would include a session nonce in the proof. This is a known omission.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;session_key&lt;/code&gt; is transmitted but not used for chat crypto.&lt;/strong&gt; After SRP, the server sends &lt;code&gt;session_key&lt;/code&gt; to the client for potential per-session keying. In the current implementation, &lt;code&gt;encrypt_text&lt;/code&gt; and &lt;code&gt;decrypt_text&lt;/code&gt; use &lt;code&gt;room_key&lt;/code&gt; (derived from the password directly), not from &lt;code&gt;session_key&lt;/code&gt;. This is documented as a known gap between the README security diagram and the actual implementation — aligning them is a clean next step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Thread-per-connection does not scale.&lt;/strong&gt; For the stated use case (small team, trusted network) this is fine. For anything beyond ~100 concurrent connections, &lt;code&gt;io_uring&lt;/code&gt; or &lt;code&gt;epoll&lt;/code&gt;/&lt;code&gt;kqueue&lt;/code&gt; with an event loop would be the right move.&lt;/p&gt;




&lt;h2&gt;
  
  
  Build and Dependency Management
&lt;/h2&gt;

&lt;p&gt;All C++ dependencies except OpenSSL are fetched at configure time via CMake &lt;code&gt;FetchContent&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cmake"&gt;&lt;code&gt;&lt;span class="nf"&gt;FetchContent_Declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;nlohmann_json
    URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;FetchContent_Declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;csrp
    GIT_REPOSITORY https://github.com/cocagne/csrp.git
    GIT_TAG        15d6bd7&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;csrp has no CMakeLists.txt of its own, so it is built manually as a static library after &lt;code&gt;FetchContent_Populate&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cmake"&gt;&lt;code&gt;&lt;span class="nb"&gt;add_library&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;csrp STATIC &lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;csrp_SOURCE_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/srp.c&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;target_link_libraries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;csrp PUBLIC OpenSSL::Crypto&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OpenSSL is the only system dependency. CI builds on GitHub Actions cover Windows (Chocolatey), Ubuntu (&lt;code&gt;libssl-dev&lt;/code&gt;), and macOS (Homebrew &lt;code&gt;openssl@3&lt;/code&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cmake &lt;span class="nt"&gt;-S&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-B&lt;/span&gt; build &lt;span class="nt"&gt;-DCMAKE_BUILD_TYPE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Release
cmake &lt;span class="nt"&gt;--build&lt;/span&gt; build &lt;span class="nt"&gt;-j&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;nproc&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

./build/cmd_chat_server serve 0.0.0.0 9000 &lt;span class="nt"&gt;--password&lt;/span&gt; roomsecret
./build/cmd_chat_client connect 127.0.0.1 9000 alice roomsecret
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What I Would Change in a Production Version
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;AES-256-GCM instead of AES-128-CBC + HMAC.&lt;/strong&gt; Simpler construction, no padding oracle surface, authenticated natively by the AEAD mode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ephemeral ECDH per session&lt;/strong&gt; (X25519) layered on top of SRP to achieve forward secrecy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TLS for the transport layer.&lt;/strong&gt; The current raw TCP is fine for a controlled environment; wrapping with &lt;code&gt;OpenSSL::SSL&lt;/code&gt; (or just &lt;code&gt;boringssl&lt;/code&gt;) would take the transport threat model off the table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-user credentials.&lt;/strong&gt; The current single-verifier model makes sense for a shared room but not for a multi-room or multi-tenant system.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;io_uring&lt;/code&gt;-based event loop on Linux&lt;/strong&gt; to replace thread-per-connection for anything that needs to scale.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Source
&lt;/h2&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/double-k-3033/cmd-chat-cpp" rel="noopener noreferrer"&gt;double-k-3033/cmd-chat-cpp&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The implementation is intentionally small — around 600 lines across all source files — so the full crypto flow is traceable from a single reading session. If you are evaluating PAKE schemes, building a custom secure channel in C++, or just want a working reference for HKDF + Fernet without an opaque wrapper library, it should be useful.&lt;/p&gt;

</description>
      <category>cpp</category>
      <category>security</category>
      <category>networking</category>
      <category>systems</category>
    </item>
  </channel>
</rss>
