<?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: Ymsniper</title>
    <description>The latest articles on DEV Community by Ymsniper (@ymsniper).</description>
    <link>https://dev.to/ymsniper</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%2F3808333%2Fc8f2f3df-7a71-4d14-97da-2fea0f2a9595.png</url>
      <title>DEV Community: Ymsniper</title>
      <link>https://dev.to/ymsniper</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ymsniper"/>
    <language>en</language>
    <item>
      <title>I Built a Chat Server That Cannot Read Your Messages — Here's How</title>
      <dc:creator>Ymsniper</dc:creator>
      <pubDate>Thu, 05 Mar 2026 16:49:50 +0000</pubDate>
      <link>https://dev.to/ymsniper/i-built-a-chat-server-that-cannot-read-your-messages-heres-how-k4n</link>
      <guid>https://dev.to/ymsniper/i-built-a-chat-server-that-cannot-read-your-messages-heres-how-k4n</guid>
      <description>&lt;h1&gt;
  
  
  How I Built a Chat Server That Can't Read Your Messages
&lt;/h1&gt;

&lt;p&gt;Most "encrypted" chat apps encrypt &lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr9ea2rzjmy634mmu5mv6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr9ea2rzjmy634mmu5mv6.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;the &lt;em&gt;connection&lt;/em&gt;. The server still decrypts every message, reads it, then re-encrypts it for the recipient. You're trusting the server operator — and every employee, contractor, and attacker who ever gets access to that machine.&lt;/p&gt;

&lt;p&gt;I wanted to build something different. A chat server that is &lt;strong&gt;mathematically incapable&lt;/strong&gt; of reading your messages — not by policy, not by promise, but by design. If you hand the server's private keys to an attacker, they still get nothing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fozl5qdsva3jjwi7mb0py.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fozl5qdsva3jjwi7mb0py.png" alt=" " width="792" height="663"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The result is &lt;strong&gt;NoEyes&lt;/strong&gt; — a terminal-based E2E encrypted chat tool with a blind-forwarder server.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/Ymsniper/NoEyes" rel="noopener noreferrer"&gt;https://github.com/Ymsniper/NoEyes&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  The Core Idea: The Blind Forwarder
&lt;/h2&gt;

&lt;p&gt;The server in NoEyes does exactly one thing: route packets. It reads a small plaintext JSON header at the front of each frame to find out where the packet should go, then forwards the encrypted payload verbatim without touching it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────────────────────────────────────┐
│  Alice ─────────────────────────────────── Bob       │
│    │        Encrypted payload (opaque)      │        │
│    │                  │                     │        │
│    └──────► SERVER ───┴◄────────────────────┘        │
│                  │                                   │
│            Blind forwarder:                          │
│            reads routing header only                 │
│            { "type":"chat", "room":"general" }       │
│            forwards encrypted bytes verbatim         │
└──────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server has no decryption keys. There are no calls to &lt;code&gt;Fernet.decrypt&lt;/code&gt; anywhere in &lt;code&gt;server.py&lt;/code&gt;. The encrypted payload is just bytes — the server doesn't know what's in it and has no way to find out.&lt;/p&gt;




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

&lt;p&gt;Each frame has a fixed 8-byte header followed by the routing JSON and the payload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[4-byte header_len BE] [4-byte payload_len BE] [header JSON] [payload bytes]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The header JSON is plaintext — just enough for routing:&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="w"&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;"chat"&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"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"general"&lt;/span&gt;&lt;span class="w"&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="w"&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;"privmsg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"to"&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="w"&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="w"&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;"dh_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;"to"&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="w"&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;The payload is always encrypted. The server never needs to look inside it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Group Chat: Per-Room Key Isolation
&lt;/h2&gt;

&lt;p&gt;The shared &lt;code&gt;chat.key&lt;/code&gt; file is not used directly for encryption. It's a master secret from which per-room keys are derived via HKDF:&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="nc"&gt;HKDF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;algorithm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;hashes&lt;/span&gt;&lt;span class="p"&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;length&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;span class="n"&gt;salt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&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="n"&gt;room_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;derive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;master_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means a user in &lt;code&gt;#general&lt;/code&gt; cannot decrypt messages from &lt;code&gt;#ops&lt;/code&gt; even if they somehow get the encrypted bytes — the keys are completely different. Rooms are cryptographically isolated.&lt;/p&gt;




&lt;h2&gt;
  
  
  Private Messages: X25519 DH Handshake
&lt;/h2&gt;

&lt;p&gt;When Alice sends her first &lt;code&gt;/msg&lt;/code&gt; to Bob, neither of them has a shared pairwise key yet. NoEyes triggers a DH handshake automatically:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Alice generates an X25519 ephemeral keypair&lt;/li&gt;
&lt;li&gt;Alice sends &lt;code&gt;dh_init&lt;/code&gt; containing her public key — &lt;em&gt;encrypted inside the group Fernet key&lt;/em&gt; so the server sees only opaque bytes&lt;/li&gt;
&lt;li&gt;Bob generates his own ephemeral keypair, derives the shared secret, and sends back &lt;code&gt;dh_resp&lt;/code&gt; with his public key&lt;/li&gt;
&lt;li&gt;Alice derives the same shared secret from Bob's public key&lt;/li&gt;
&lt;li&gt;Both sides now hold an identical pairwise Fernet key — the server never saw either private key or the shared secret&lt;/li&gt;
&lt;li&gt;The original message is automatically re-sent encrypted with the new pairwise key
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;shared_secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;alice_private&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bob_public&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;pairwise_key&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;shared_secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From this point on, all &lt;code&gt;/msg&lt;/code&gt; traffic between Alice and Bob is encrypted with a key the server has never seen and cannot compute.&lt;/p&gt;




&lt;h2&gt;
  
  
  Identity and Signatures: Ed25519 + TOFU
&lt;/h2&gt;

&lt;p&gt;Every client generates an Ed25519 keypair on first run, stored at &lt;code&gt;~/.noeyes/identity.key&lt;/code&gt;. Every private message payload is signed:&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;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ed25519_private_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Recipients verify against the sender's public key. Public keys are announced to the room over the server, but — again — the server only routes the announcement, it doesn't process it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TOFU (Trust On First Use):&lt;/strong&gt; the first time Alice sees Bob's public key, it gets stored in &lt;code&gt;~/.noeyes/tofu_pubkeys.json&lt;/code&gt;. Every subsequent message from Bob is verified against that stored key. If Bob's key ever changes — reinstalled client, new machine — Alice sees a loud warning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;⚠  SECURITY WARNING
   Key mismatch for bob
   Expected: a3f9... | Got: 7c12...
   Use /trust bob to accept the new key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the same trust model Signal uses. You're warned immediately if something changed.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Bug That Taught Me the Most
&lt;/h2&gt;

&lt;p&gt;The trickiest bug was in the &lt;code&gt;/trust&lt;/code&gt; command. When Bob reconnected with a new identity, Alice would run &lt;code&gt;/trust bob&lt;/code&gt; — but Bob's new key still never verified correctly.&lt;/p&gt;

&lt;p&gt;The problem: when the TOFU mismatch was detected, the new key was added to a &lt;code&gt;_tofu_mismatched&lt;/code&gt; set but &lt;strong&gt;not cached anywhere&lt;/strong&gt;. The &lt;code&gt;/trust&lt;/code&gt; command deleted the old key from storage but had nothing to save in its place. Bob's new key was just gone.&lt;/p&gt;

&lt;p&gt;The fix was a &lt;code&gt;_tofu_pending&lt;/code&gt; dict — when a mismatch fires, cache the new key immediately:&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="c1"&gt;# On mismatch detection:
&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_tofu_pending&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_vk_hex&lt;/span&gt;
&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_tofu_mismatched&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# On /trust:
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;peer&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_tofu_pending&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;new_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_tofu_pending&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;peer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tofu_store&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;peer&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_key&lt;/span&gt;
    &lt;span class="nf"&gt;save_tofu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tofu_store&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A simple fix. But finding it required understanding exactly when keys flow through the system and where they get dropped.&lt;/p&gt;

&lt;p&gt;There was also a DH deadlock edge case: if Alice and Bob both send &lt;code&gt;/msg&lt;/code&gt; to each other at the exact same millisecond, both trigger &lt;code&gt;dh_init&lt;/code&gt; simultaneously. Without a tiebreaker, both wait for the other to respond and nobody ever does. The fix is lexicographic: whoever has the alphabetically lower username is the initiator, the other becomes the responder. Two lines of code, but the kind of thing you only think of after staring at a deadlock for an hour.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the Server Actually Sees
&lt;/h2&gt;

&lt;p&gt;To make this concrete — here's what a full packet capture of a NoEyes session reveals to the server:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What the server sees&lt;/th&gt;
&lt;th&gt;What the server cannot see&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Usernames&lt;/td&gt;
&lt;td&gt;Message content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Room names&lt;/td&gt;
&lt;td&gt;File contents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Event types (join/leave)&lt;/td&gt;
&lt;td&gt;Private message bodies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frame byte length&lt;/td&gt;
&lt;td&gt;DH key exchange values&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timestamp of events&lt;/td&gt;
&lt;td&gt;Ed25519 signatures&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Pairwise keys&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The server is a router. It knows who is talking to whom and when. It does not know what they are saying.&lt;/p&gt;




&lt;h2&gt;
  
  
  Running It
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Ymsniper/NoEyes
&lt;span class="nb"&gt;cd &lt;/span&gt;NoEyes
python setup.py          &lt;span class="c"&gt;# installs cryptography, optionally bore&lt;/span&gt;

&lt;span class="c"&gt;# Generate a shared key — share this out of band&lt;/span&gt;
python noeyes.py &lt;span class="nt"&gt;--gen-key&lt;/span&gt; &lt;span class="nt"&gt;--key-file&lt;/span&gt; ./chat.key

&lt;span class="c"&gt;# Start the server&lt;/span&gt;
python noeyes.py &lt;span class="nt"&gt;--server&lt;/span&gt; &lt;span class="nt"&gt;--port&lt;/span&gt; 5000

&lt;span class="c"&gt;# Connect&lt;/span&gt;
python noeyes.py &lt;span class="nt"&gt;--connect&lt;/span&gt; SERVER_IP &lt;span class="nt"&gt;--port&lt;/span&gt; 5000 &lt;span class="nt"&gt;--username&lt;/span&gt; alice &lt;span class="nt"&gt;--key-file&lt;/span&gt; ./chat.key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One dependency. Pure Python. Works on Linux, macOS, Windows, Termux (Android), and iSH (iOS).&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd Love Feedback On
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The trust model — is TOFU the right default, or should there be a stricter option?&lt;/li&gt;
&lt;li&gt;The wire protocol — anything obviously wrong with the framing format?&lt;/li&gt;
&lt;li&gt;The DH handshake — I'm using ephemeral keys but not doing a full double ratchet. Is that a meaningful gap for this use case?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm self-taught and this is the most serious security-adjacent thing I've built. I'd genuinely appreciate eyes from people who know this space better than I do.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/Ymsniper/NoEyes" rel="noopener noreferrer"&gt;https://github.com/Ymsniper/NoEyes&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>opensource</category>
      <category>python</category>
      <category>linux</category>
    </item>
  </channel>
</rss>
