<?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: Erkin Khidirov</title>
    <description>The latest articles on DEV Community by Erkin Khidirov (@erkin_khidirov_e1a1d8dc51).</description>
    <link>https://dev.to/erkin_khidirov_e1a1d8dc51</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3121912%2F4d0905d0-b2a3-4b60-9634-645cda95e93e.png</url>
      <title>DEV Community: Erkin Khidirov</title>
      <link>https://dev.to/erkin_khidirov_e1a1d8dc51</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/erkin_khidirov_e1a1d8dc51"/>
    <language>en</language>
    <item>
      <title>How I wrote a Go message broker with a throughput of a million messages per second</title>
      <dc:creator>Erkin Khidirov</dc:creator>
      <pubDate>Tue, 18 Aug 2026 21:15:58 +0000</pubDate>
      <link>https://dev.to/erkin_khidirov_e1a1d8dc51/how-i-wrote-a-go-message-broker-with-a-throughput-of-a-million-messages-per-second-o86</link>
      <guid>https://dev.to/erkin_khidirov_e1a1d8dc51/how-i-wrote-a-go-message-broker-with-a-throughput-of-a-million-messages-per-second-o86</guid>
      <description>&lt;p&gt;I built HermitMQ entirely in Go. The main feature is ditching heavy wrappers like JSON in favor of a custom 29 byte binary protocol. Additionally, data transmission over the network uses a direct file to socket copy mechanism. I will go into detail about the architecture, data storage approaches, benchmark numbers, and show how it is implemented in code.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The full source code for the HermitMQ project is available on GitHub:&lt;/em&gt;    &lt;a href="https://github.com/ekhidirov/hermitmq" rel="noopener noreferrer"&gt;https://github.com/ekhidirov/hermitmq&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with standard brokers and the cost of serialization
&lt;/h2&gt;

&lt;p&gt;When the message counter exceeds hundreds of thousands per second, the main problem for a Go developer is the garbage collector. If every message is parsed via standard JSON, the application starts allocating a massive number of small objects in memory. The GC wakes up too frequently, eating up CPU time and causing network latency spikes.&lt;/p&gt;

&lt;p&gt;To avoid triggering the garbage collector at every turn, I completely abandoned standard serialization libraries. Every message is packed into a custom header of exactly 29 bytes. In code, the message structure looks extremely simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Message&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Magic&lt;/span&gt;       &lt;span class="kt"&gt;byte&lt;/span&gt;
    &lt;span class="n"&gt;Timestamp&lt;/span&gt;   &lt;span class="kt"&gt;uint64&lt;/span&gt;
    &lt;span class="n"&gt;Offset&lt;/span&gt;      &lt;span class="kt"&gt;uint64&lt;/span&gt;
    &lt;span class="n"&gt;KeySize&lt;/span&gt;     &lt;span class="kt"&gt;uint32&lt;/span&gt;
    &lt;span class="n"&gt;PayloadSize&lt;/span&gt; &lt;span class="kt"&gt;uint32&lt;/span&gt;
    &lt;span class="n"&gt;RecordCount&lt;/span&gt; &lt;span class="kt"&gt;uint32&lt;/span&gt;
    &lt;span class="n"&gt;Key&lt;/span&gt;         &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;
    &lt;span class="n"&gt;Payload&lt;/span&gt;     &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first byte is a magic number for version checking and instantly discarding bad packets. Next come 8 bytes for the timestamp in nanoseconds and 8 bytes for the offset, which the broker fills in itself to maintain message order. Then come the key and payload sizes, 4 bytes each. Finally, 4 bytes are reserved for the record count to support batching. The broker reads the stream using the binary package and reuses buffers via sync.Pool. As a result, under standard loads, we achieve practically zero memory allocation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Being honest about allocations and plans for zero serialization
&lt;/h2&gt;

&lt;p&gt;To be completely honest: although the broker is incredibly frugal under standard loads, a memory management compromise still remains.&lt;/p&gt;

&lt;p&gt;An absolute victory over allocations and the GC has been achieved on the side of serving data to consumers, thanks to io.CopyN (the very zero copy approach I will discuss below). However, on the receiving end from producers, the Decode method still dynamically allocates slices for the payload size (make([]byte, m.PayloadSize)).&lt;/p&gt;

&lt;p&gt;In future releases, I plan to completely eliminate this structure. We will move towards the concept of zero serialization (like in flatbuffers) and micro optimizations with branchless logic in hot loops. The broker will access byte offsets directly in the network buffer, without unpacking them into Go structs at all. This will finally eliminate the GC from the message processing path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Direct data transfer at the kernel level
&lt;/h2&gt;

&lt;p&gt;Copying data from the OS kernel into user space and back to the network interface is very expensive. It is double the work for the CPU and RAM. In HermitMQ, consumers receive data directly.&lt;/p&gt;

&lt;p&gt;When a client requests data, the broker looks up the offset in the log file. Instead of reading the file into the program memory, it uses the built in io.CopyN function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;bytesToSend&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HeaderSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keySize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payloadSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewSectionReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;walFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bytesToSend&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CopyN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bytesToSend&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;Data&lt;/span&gt; &lt;span class="n"&gt;flows&lt;/span&gt; &lt;span class="n"&gt;straight&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;physical&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="n"&gt;disk&lt;/span&gt; &lt;span class="n"&gt;into&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;consumer&lt;/span&gt; &lt;span class="n"&gt;network&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Latencies&lt;/span&gt; &lt;span class="n"&gt;remain&lt;/span&gt; &lt;span class="n"&gt;minimal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;and&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;CPU&lt;/span&gt; &lt;span class="n"&gt;does&lt;/span&gt; &lt;span class="n"&gt;not&lt;/span&gt; &lt;span class="n"&gt;strain&lt;/span&gt; &lt;span class="n"&gt;itself&lt;/span&gt; &lt;span class="n"&gt;moving&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt; &lt;span class="n"&gt;around&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Data storage, mmap, and compaction
&lt;/h2&gt;

&lt;p&gt;All data is written to a write ahead log (WAL). The log is divided into segments. Each segment has a .wal file with raw data and an .idx file to map offsets to physical bytes on the hard drive.&lt;/p&gt;

&lt;p&gt;For fast searching, indices are mapped directly into RAM via mmap. The broker performs a binary search in memory and instantly finds the required byte. Background log compaction is also implemented here. It deduplicates messages by key, leaving only the most relevant data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Surviving crashes: the torn page recovery mechanism
&lt;/h2&gt;

&lt;p&gt;Any database developer knows: a server can crash at any millisecond due to a power failure or a hard kill 9 signal. If this happens during a log write, a half written chunk of data (a torn page) will be left at the end of the file.&lt;/p&gt;

&lt;p&gt;HermitMQ implements an automatic recovery mechanism. On startup, the broker sequentially scans the WAL files. If it sees that the expected message size in the header exceeds the physical size of the file on disk, it understands that the write was interrupted. Instead of panicking and crashing the entire segment, the broker safely truncates this corrupted tail at the hardware level using os.Truncate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;currentPos&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;msgSize&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;totalFileSize&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"PARTIAL MESSAGE DETECTED. Truncating tail."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;break&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// ...&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Truncate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;walPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currentPos&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This guarantees that the broker will boot up with a completely consistent state, even after the most severe operating system crash.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lock sharding and scaling
&lt;/h2&gt;

&lt;p&gt;Storing consumer group offsets under a single global mutex is a bad idea when dealing with thousands of connections. Threads will start queuing up and kill all performance. Therefore, the offset store is split into 256 independent shards.&lt;/p&gt;

&lt;p&gt;The broker takes a string key, runs it through a hashing algorithm, and routes it to the correct shard. Here is the implementation of this hash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;getShardIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;uint8&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;hash&lt;/span&gt; &lt;span class="kt"&gt;uint32&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;2166136261&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;^=&lt;/span&gt; &lt;span class="kt"&gt;uint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="m"&gt;16777619&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;uint8&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OffsetShardCount&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Incoming requests are evenly spread across the allocated memory. Producers and consumers do not block each other, and the broker scales perfectly across CPU cores.&lt;/p&gt;

&lt;h2&gt;
  
  
  Developer experience: on demand routing
&lt;/h2&gt;

&lt;p&gt;Working with hardcore systems is often painful due to excessive infrastructure bureaucracy, you usually have to hit an API in advance to configure topics and partitions. I solved this problem through on demand routing.&lt;/p&gt;

&lt;p&gt;If a producer or consumer knocks on the broker and requests a non existent topic, the broker does not reject the connection with an error. Instead, it transparently creates the required folder and file structure on the fly under an RWMutex lock.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RLock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exists&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;topics&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;topicName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RUnlock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;exists&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;slog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"auto-creating new topic on demand"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"topic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;topicName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateTopic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;topicName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client simply needs to start writing data, the entire physical structure under the hood will spin up on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Smart batching on the client
&lt;/h2&gt;

&lt;p&gt;Throughput depends not only on the server but also on the clients. In the project, the test producer uses Go channels and tickers to group messages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ProducersCount&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
    &lt;span class="n"&gt;MessagesPerWorker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;100000&lt;/span&gt;
    &lt;span class="n"&gt;BatchSize&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;
    &lt;span class="n"&gt;LingerTime&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Millisecond&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Workers buffer messages and flush them to the TCP socket only when two conditions are met: a batch of 100 records has accumulated, or a 100 millisecond wait timer expires. This glues a hundred logical records into a single dense header and a single system write call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmark numbers and stress test results
&lt;/h2&gt;

&lt;p&gt;To measure the real throughput of the pipeline, I set up a cluster of 10 parallel producers and a separate consumer. The test scenario pumps 1,000,000 messages through the broker.&lt;/p&gt;

&lt;p&gt;Producers generate 10 threads of 100,000 messages each. The consumer fetches the incoming stream, parses the 29 byte headers, and calculates the net throughput over time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;totalMB&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logicalCount&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1812&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="m"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="m"&gt;1024&lt;/span&gt;
&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Throughput: %.0f msgs/sec (Approx %.0f MB/sec)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logicalCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;elapsed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Seconds&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; 
    &lt;span class="n"&gt;totalMB&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;elapsed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Seconds&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With a batch of 1,000,000 messages, the entire volume of data is pumped through in a matter of seconds. Thanks to zero copy and the complete absence of intermediate memory allocations, the broker delivers hundreds of thousands of messages per second and utilizes tens of megabytes of traffic per second on a single instance, bottlenecked solely by the physical speed of the disk bus and the local TCP stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Overload protection and security
&lt;/h2&gt;

&lt;p&gt;The broker is protected against memory exhaustion attacks by strict limits. The payload size is strictly validated before memory is allocated; by default, the limit is set to 50 megabytes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;maxBytes&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="kt"&gt;uint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxPayloadMB&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PayloadSize&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;maxBytes&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"security policy violation: payload too large"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the size exceeds the limit or a packet arrives with a bad starting byte, the broker does not crash with an out of memory error; it simply drops the malicious connection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Graceful shutdown
&lt;/h2&gt;

&lt;p&gt;High delivery speed is meaningless if data stuck in OS buffers is lost during a restart. That is why the broker knows how to respond correctly to system interrupt signals.&lt;/p&gt;

&lt;p&gt;Interception of SIGINT and SIGTERM signals is implemented in main.go. Upon receiving a signal, the broker stops accepting new connections, closes the TCP listener, and neatly iterates through all topics and partitions. For each open segment, Sync() and Close() are forcibly called, flushing all file descriptors to disk.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;sigChan&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sigChan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;syscall&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;syscall&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SIGTERM&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;sigChan&lt;/span&gt;
&lt;span class="n"&gt;slog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"received shutdown signal, shutting down..."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;listener&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Writing your own message broker is excellent practice for understanding how high load systems work at a low level. The architecture allows implementing direct memory management, data streaming, and concurrency without unnecessary headaches. Ditching fat frameworks gives full control over every allocated byte and provides predictable performance under any load.&lt;/p&gt;

</description>
      <category>hermitmq</category>
      <category>json</category>
      <category>go</category>
      <category>devops</category>
    </item>
    <item>
      <title>How Messengers Actually Encrypt Messages (End-to-End)</title>
      <dc:creator>Erkin Khidirov</dc:creator>
      <pubDate>Thu, 29 May 2025 22:34:20 +0000</pubDate>
      <link>https://dev.to/erkin_khidirov_e1a1d8dc51/how-messengers-actually-encrypt-messages-end-to-end-5a52</link>
      <guid>https://dev.to/erkin_khidirov_e1a1d8dc51/how-messengers-actually-encrypt-messages-end-to-end-5a52</guid>
      <description>&lt;p&gt;Hi everyone!&lt;/p&gt;

&lt;p&gt;I've written an article and I'm publishing it prematurely. Initially, I planned to write it after the project was completed, but since there are still a couple of months left until the end, I decided not to waste time and write the article while the information is still fresh in my mind. Besides, I'm mostly writing this for myself. :) In one of my latest projects, which I'm developing as open source, I implemented end-to-end encryption—similar to how WhatsApp or Telegram do it, for example.&lt;/p&gt;

&lt;p&gt;In this article, we'll dive into the implementation of client-side message encryption using JavaScript and the Web Crypto API, breaking down a practical example that will be at the very end of the article.&lt;/p&gt;

&lt;p&gt;Let's start by saying that if you're a complete beginner in cryptography, understanding what's written here might not be easy. Even with 10 years of development experience, I had to scratch my head a bit—everything happening here is pure mathematics, which we won't be discussing in this article :) The easily impressed might think it's magic :)&lt;/p&gt;

&lt;p&gt;If I were to briefly explain the essence of end-to-end encryption without complex words and terms:&lt;/p&gt;

&lt;h2&gt;
  
  
  The Magic of Encryption in Three Keys
&lt;/h2&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%2F3sr7j0bqnqb7kdbwk2ok.jpg" 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%2F3sr7j0bqnqb7kdbwk2ok.jpg" alt=" " width="800" height="449"&gt;&lt;/a&gt;&lt;br&gt;
Three keys are the foundation upon which end-to-end encryption is built. Pay attention to the key in the center – this is important.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The foundation on which everything rests is these three keys. Refer back to this section if something is unclear.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;Private Key: Stored (in encrypted form).&lt;/li&gt;
&lt;li&gt;Public Key: Accessible to everyone.&lt;/li&gt;
&lt;li&gt;Shared Secret / Symmetric Key: Generated based on your private key + your contact's public key. This is the key used for the direct encryption and decryption of messages.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The combination of your private key + your contact's public key allows you to obtain the shared secret key (in our example below, this will be an AES key). Thanks to this shared secret key, you can encrypt and decrypt messages.&lt;/p&gt;

&lt;p&gt;Private and public keys can be stored in a database, but there's a nuance with the private key. The private key itself is not recommended to be stored in plain text; it needs to be additionally encrypted with the user's password or any other keyword (within a messenger, this is typically the user's password). We store the public key in plain text.&lt;/p&gt;

&lt;p&gt;The shared secret key (the one referred to as this.aesKey in the code below) is not stored in the database. It is generated (calculated) each time a chat with a specific contact is initialized. This might cause confusion: how will we decrypt messages if this key is not stored but generated anew? This is where the "magic" of asymmetric encryption and key exchange protocols lies.&lt;/p&gt;

&lt;p&gt;The shared key is "Your private key" + "Contact's public key."&lt;/p&gt;

&lt;p&gt;When you open a chat with a contact, your client recalculates this shared secret key, as you already know, like this: (Your private key + Your contact's public key = Shared secret key). With this key, you encrypt new messages and decrypt all previous messages in this chat, as they were encrypted with the same shared secret key. You can wrack your brains for a long time to no avail until you understand asymmetric encryption and key exchange protocols.&lt;/p&gt;
&lt;h2&gt;
  
  
  Asymmetric Encryption and ECDH
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Asymmetric encryption&lt;/strong&gt; uses a pair of keys: public and private. The public key can be freely distributed, while the private key must be kept secret by its owner, meaning in an encrypted form.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ECDH (Elliptic Curve Diffie-Hellman)&lt;/strong&gt; is a key exchange protocol based on elliptic curve mathematics. It allows two parties, each possessing their own ECDH key pair (private and public), to establish a shared secret key over an insecure channel. Importantly, a third party, even if they intercept their public keys, cannot compute this shared secret. Our example uses the P-256 curve – a popular and reliable standard.&lt;/p&gt;

&lt;p&gt;I think few understood what they just read. All you need to understand at this stage is that the technology works :) Later, the puzzle will come together, perhaps after a reread. And now, a bit about built-in browser technologies.&lt;/p&gt;
&lt;h2&gt;
  
  
  Web Crypto API
&lt;/h2&gt;

&lt;p&gt;The Web Crypto API is a JavaScript interface built into browsers that provides access to low-level cryptographic primitives. It allows performing operations such as hashing, signature generation, encryption, and decryption. Using the Web Crypto API is preferable to third-party libraries for basic cryptographic operations, as it is often hardware-accelerated and thoroughly vetted for security. All Web Crypto API operations are asynchronous and return a Promise.&lt;/p&gt;

&lt;p&gt;Now let's move on to the practical analysis. I've created a ChatCrypto class, which we'll examine in more detail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class ChatCrypto {

  constructor(myPrivateKeyBase64, theirPublicKeyBase64) {
    this.myPrivateKeyBase64 = myPrivateKeyBase64;
    this.theirPublicKeyBase64 = theirPublicKeyBase64;
    this.aesKey = null; // The shared symmetric AES key will be stored here
  }

  static base64ToArrayBuffer(base64) {
    const binary = atob(base64);
    const bytes = new Uint8Array(binary.length);
    for (let i = 0; i &amp;lt; binary.length; i++) {
      bytes[i] = binary.charCodeAt(i);
    }
    return bytes.buffer;
  }

  static arrayBufferToBase64(buffer) {
    const bytes = new Uint8Array(buffer);
    let binary = '';
    for (let b of bytes) {
      binary += String.fromCharCode(b);
    }
    return btoa(binary);
  }

  init() {

    // Convert keys from Base64 to ArrayBuffer
    const privateRaw = ChatCrypto.base64ToArrayBuffer(this.myPrivateKeyBase64);
    const publicRaw = ChatCrypto.base64ToArrayBuffer(this.theirPublicKeyBase64);

    // Note: The following lines for parsing publicRaw into x and y coordinates,
    // and assembling uncompressedPoint might be specific to a particular format
    // for representing a "raw" public key. If publicRaw is already in SPKI format,
    // they might not be necessary, as crypto.subtle.importKey("spki", ...)
    // expects a standard structure.
    // const x = publicRaw.slice(0, publicRaw.byteLength / 2);
    // const y = publicRaw.slice(publicRaw.byteLength / 2);
    // const uncompressedPoint = new Uint8Array([0x04, ...new Uint8Array(x), ...new Uint8Array(y)]);

    // Import our private key
    return crypto.subtle.importKey(
      "pkcs8", // Private key format (standard)
      privateRaw,
      { name: "ECDH", namedCurve: "P-256" }, // Algorithm and parameters
      false, // Non-exportable
      ["deriveBits"] // Allowed usage: for deriving bits (shared secret)
    ).then(privateKey =&amp;gt; {
      // Import the contact's public key
      return crypto.subtle.importKey(
        "spki", // Public key format (standard)
        publicRaw,
        { name: "ECDH", namedCurve: "P-256" },
        false, // Non-exportable
        [] // Specific uses are not needed here for the public key in ECDH
      ).then(publicKey =&amp;gt; {
        // 4. Compute the shared secret (deriveBits)
        return crypto.subtle.deriveBits(
          { name: "ECDH", public: publicKey }, // Specify the contact's public key
          privateKey, // Our private key
          256 // Length of the derived secret in bits
        );
      });
    }).then(sharedBits =&amp;gt; {
      // Hash the shared secret to obtain an AES key (using SHA-256 as KDF)
      return crypto.subtle.digest("SHA-256", sharedBits);
    }).then(hashed =&amp;gt; {
      // Import the hashed secret as an AES-GCM key
      return crypto.subtle.importKey(
        "raw", // "Raw" byte format
        hashed, // Hashed secret
        { name: "AES-GCM" }, // Symmetric encryption algorithm
        false, // Non-exportable
        ["encrypt", "decrypt"] // Allowed uses: encryption and decryption
      );
    }).then(aesKey =&amp;gt; {
      this.aesKey = aesKey; // Save the obtained AES key
      return true;         // Signal successful initialization
    });
  }

  encrypt(plaintext) {
    if (!this.aesKey) return Promise.reject("ChatCrypto not initialized");

    // Generate a unique initialization vector (IV)
    const iv = crypto.getRandomValues(new Uint8Array(12)); // 12 bytes (96 bits) is recommended for AES-GCM

    // Convert the text message to bytes (UTF-8)
    const encoded = new TextEncoder().encode(plaintext);

    // Encrypt the data
    return crypto.subtle.encrypt(
      { name: "AES-GCM", iv: iv }, // Algorithm and IV
      this.aesKey, // Our shared AES key
      encoded // Data to encrypt
    ).then(encrypted =&amp;gt; {
      // 4. Return IV and encrypted data (in Base64 for convenient transmission)
      return {
        iv: ChatCrypto.arrayBufferToBase64(iv),
        data: ChatCrypto.arrayBufferToBase64(encrypted)
      };
    });
  }

  decrypt(cipherBase64, ivBase64) {
    if (!this.aesKey) return Promise.reject("ChatCrypto not initialized");

    // Convert ciphertext and IV from Base64 to ArrayBuffer
    const encrypted = ChatCrypto.base64ToArrayBuffer(cipherBase64);
    const ivBuffer = ChatCrypto.base64ToArrayBuffer(ivBase64);

    // Decrypt the data
    return crypto.subtle.decrypt(
      { name: "AES-GCM", iv: new Uint8Array(ivBuffer) }, // Algorithm and IV (must be a TypedArray)
      this.aesKey, // The same shared AES key
      encrypted // Encrypted data
    ).then(decrypted =&amp;gt; {
      // Convert decrypted bytes back to a string
      return new TextDecoder().decode(decrypted);
    });
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;constructor() and base64ToArrayBuffer() method:&lt;br&gt;
The constructor accepts your private key and the contact's public key in Base64 format. Base64 is a way of encoding binary data into a text string, convenient for transmission or storage.&lt;/p&gt;

&lt;p&gt;this.aesKey is initialized as null and will be populated after the init() method successfully completes.&lt;/p&gt;

&lt;p&gt;The static methods base64ToArrayBuffer and arrayBufferToBase64 are used to convert data between Base64 strings and ArrayBuffer (the format Web Crypto API works with).&lt;/p&gt;

&lt;p&gt;init() Method: Establishing the Shared AES Key&lt;br&gt;
This is the heart of our class, where the "magic" of ECDH occurs and the shared key for symmetric encryption is created.&lt;/p&gt;

&lt;p&gt;Breakdown of steps in init():&lt;/p&gt;

&lt;p&gt;Key Conversion: Keys are converted from Base64 to ArrayBuffer.&lt;br&gt;
Import Private Key: Your private key is imported in pkcs8 format. It's specified that this is an ECDH key on the P-256 curve and will be used for deriveBits (computing the shared secret).&lt;/p&gt;

&lt;p&gt;Import Contact's Public Key: The contact's public key is imported in spki format.&lt;/p&gt;

&lt;p&gt;Compute Shared Secret (deriveBits): This is the key ECDH step. Using your private key and the contact's public key, deriveBits computes a shared secret set of bits (sharedBits). This secret will be the same for you and your contact if they use their respective private keys and each other's public keys.&lt;/p&gt;

&lt;p&gt;Hash Shared Secret (digest): sharedBits are hashed using SHA-256. This is a common practice to transform the output of deriveBits into a cryptographically strong key of the desired length for a symmetric cipher (AES in this case). This step also serves as a KDF (Key Derivation Function).&lt;/p&gt;

&lt;p&gt;Import AES Key (importKey): The resulting hash (hashed) is imported as a "raw" key for the AES-GCM algorithm. This key (this.aesKey) is now ready to be used for encrypting and decrypting messages.&lt;br&gt;
After successful execution, this.aesKey will contain a CryptoKey object, ready for use.&lt;/p&gt;

&lt;p&gt;encrypt(plaintext) Method: Encrypting a Message&lt;br&gt;
Breakdown of steps in encrypt():&lt;/p&gt;

&lt;p&gt;Generate IV (Initialization Vector): A random 12-byte IV is created. As a reminder, it must be unique for each encryption with the same key.&lt;/p&gt;

&lt;p&gt;Encode Text: The message is converted from a JavaScript string to a Uint8Array (a sequence of bytes in UTF-8 encoding) using TextEncoder.&lt;br&gt;
Encryption: crypto.subtle.encrypt performs data encryption using AES-GCM, our this.aesKey, and the generated iv.&lt;/p&gt;

&lt;p&gt;Return Result: The encrypted data and IV (both in Base64) are returned as an object. The IV must be transmitted to the recipient along with the ciphertext, as it will be required for decryption.&lt;br&gt;
decrypt(cipherBase64, ivBase64) Method: Decrypting a Message&lt;br&gt;
Breakdown of steps in decrypt():&lt;/p&gt;

&lt;p&gt;Data Conversion: The received ciphertext and IV (in Base64) are converted back to ArrayBuffer. Note that for crypto.subtle.decrypt, the iv parameter must be a TypedArray (e.g., Uint8Array), so we pass new Uint8Array(ivBuffer).&lt;/p&gt;

&lt;p&gt;Decryption: crypto.subtle.decrypt performs the decryption. Importantly, AES-GCM will not only decrypt the data but also verify its integrity and authenticity, using the same aesKey and iv that were used for encryption. If the data has been tampered with, the key is wrong, or the IV is wrong, the decrypt method will return an error (reject the Promise).&lt;/p&gt;

&lt;p&gt;Decode Text: Successfully decrypted bytes are converted back into a readable string using TextDecoder.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;How It Works Together: Conceptual Flow
Key Pair Generation:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;User A generates their ECDH key pair (public PkA and private SkA).&lt;br&gt;
User B does the same (public PkB and private SkB). This step is not shown in the provided ChatCrypto code but is shown below in the examples section (it is performed once, for example, during user registration), and it precedes the use of the class. The Web Crypto API has a crypto.subtle.generateKey method for this. The private key Sk must be stored securely and encrypted with the user's password.&lt;br&gt;
Public Key Exchange:&lt;/p&gt;

&lt;p&gt;User A transmits their public key PkA to User B.&lt;br&gt;
User B transmits their public key PkB to User A. This exchange must be secure to avoid Man-in-the-Middle (MitM) attacks. For example, via a secure server or by verifying key fingerprints.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ChatCrypto Initialization and Shared Secret Key Computation:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On User A's side: const cryptoA = new ChatCrypto(SkA_base64, PkB_base64); await cryptoA.init();&lt;br&gt;
On User B's side: const cryptoB = new ChatCrypto(SkB_base64, PkA_base64); await cryptoB.init(); As a result, both (cryptoA.aesKey and cryptoB.aesKey) will have computed the same symmetric AES key.&lt;br&gt;
Message Exchange:&lt;/p&gt;

&lt;p&gt;User A encrypts a message for B: const { iv, data } = await chatCryptoA.encrypt("Hello, B!"); Then A sends the { iv, data } object to user B.&lt;/p&gt;

&lt;p&gt;User B receives { iv, data } and decrypts: const message = await chatCryptoB.decrypt(data, iv); // message will be "Hello, B!"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example: Encrypting Text encrypt()&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Initialize the class
let chatCrypto = new ChatCrypto( "My_private_key" , "Contact_public_key" );

// Run it
chatCrypto.init().then(() =&amp;gt; {
   chatCrypto.encrypt("Text").then(result =&amp;gt; {

     // The encrypted text will be output
      console.log(result);

  });
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Example: Decrypting Text decrypt()&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Initialize the class
let chatCrypto = new ChatCrypto( "My_private_key" , "Contact_public_key" );

// Run it
chatCrypto.init()
  .then( () =&amp;gt; chatCrypto.decrypt("Encrypted_text", "Vector_key_iv") ) // Note: "Vector_key" likely means IV
  .then(result =&amp;gt; {

    // The decrypted text will be output
    console.log(result);

  })
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;We've looked at how to implement robust end-to-end message encryption in JavaScript using the Web Crypto API. The combination of ECDH for secure key exchange and AES-GCM for efficient and authenticated data encryption is a powerful and modern approach. The ChatCrypto class serves as a good starting example of such an implementation. Remember the importance of secure generation, storage of private keys, and reliable exchange of public keys to build a secure system.&lt;/p&gt;

</description>
      <category>security</category>
      <category>programming</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
