Skip to main content

SimpleGo Protocol Analysis

SimpleX Protocol Analysis - Part 15: Session 18

Bug #18 Solved: SMP Block-Padding in envelope_len, E2E Layer 2 Decrypt Success

Document Version: v2 (rewritten for clarity, March 2026) Date: 2026-02-05 Status: COMPLETED -- Bug #18 solved, 15904 bytes decrypted Previous: Part 14 - Session 17 Next: Session 19 (AgentConfirmation parsing) Project: SimpleGo - ESP32 Native SimpleX Client License: AGPL-3.0


SESSION 18 SUMMARY

Session 18 solved Bug #18 after seven sessions of debugging (Sessions
12-18, spanning January 30 to February 5). The root cause was a
single line: envelope_len was calculated from plain_len - 2 instead
of using the actual raw_len_prefix value from the 2-byte length
prefix. This included 102 bytes of SMP block-padding (0x23) in the
envelope data, corrupting all subsequent offsets and producing MAC
mismatches. Claude Code analyses revealed that corrId does not exist
in ClientMsgEnvelope (it is SMP Transport Layer), and Contact Queue
has no E2E Layer 2 (only server-level decryption). The one-line fix
produced 15904 bytes of decrypted AgentConfirmation content.

Root cause: 102 bytes SMP padding in envelope_len
Fix: envelope_len = raw_len_prefix (one line)
E2E Layer 2 decrypt success: 15904 bytes
corrId is SMP Transport, not in ClientMsgEnvelope
Contact Queue has no E2E Layer 2 (earlier assumption wrong)

Root Cause: SMP Block-Padding in envelope_len

The Numbers

plain_len:      16106 (total decrypted bytes from Layer 1)
raw_len_prefix: 16002 (actual ClientMsgEnvelope length from 2-byte prefix)
prefix_bytes: 2 (the length prefix itself)
padding: 102 (16106 - 16002 - 2 = SMP block-padding, value 0x23)

The Bug

// WRONG: includes 102 bytes of SMP padding
size_t envelope_len = plain_len - rq_prefix_len;

// CORRECT: uses actual content length from prefix
size_t envelope_len = raw_len_prefix;

SMP protocol pads messages to fixed block sizes for traffic analysis resistance. The padding is added after the ClientMsgEnvelope content but before Layer 1 encryption. The 2-byte length prefix tells the receiver exactly how many bytes are actual content. Using plain_len - header instead of the length prefix value included the padding in the envelope data, shifting all field boundaries and producing MAC mismatches in every decrypt attempt.

Why Contact Queue Worked

Contact Queue parser correctly used the length prefix value for content boundaries. Reply Queue parser incorrectly used buffer size minus header size. This is why Contact Queue E2E appeared to work while Reply Queue E2E always failed.


ClientMsgEnvelope Wire-Format (Claude Code Analysis)

corrId does not exist in ClientMsgEnvelope. It is part of the SMP Transport Layer, parsed before the envelope. Fields are concatenated directly (no comma separators): smpEncode a <> smpEncode b.

With e2ePubKey (first message on a queue)

[0-1]   phVersion (Word16 BE, e.g. 00 04)
[2] '1' (0x31) = Just
[3-46] X25519 SPKI (44 bytes)
[47-70] cmNonce (24 bytes, raw)
[71+] cmEncBody (Tail, rest of data)

Without e2ePubKey (subsequent messages)

[0-1]   phVersion (Word16 BE, e.g. 00 04)
[2] '0' (0x30) = Nothing
[3-26] cmNonce (24 bytes, raw)
[27+] cmEncBody (Tail, rest of data)

Complete Wrapper Chain

Layer 1 decrypt output: [2-byte len prefix][ClientMsgEnvelope][padding 0x23...]
Use len prefix, NOT buffer size!

ClientRcvMsgBody: {msgTs :: SysTime, msgFlags :: Word8, msgBody :: Tail ByteString}

ClientMsgEnvelope (inside msgBody): (PubHeader, cmNonce, Tail cmEncBody)

PubHeader: (phVersion, Maybe phE2ePubDhKey)

Contact Queue vs Reply Queue Architecture (Corrected)

Contact Queue: Only Layer 1

Incoming MSG on Contact Queue:
1. SMP Transport decrypt (Server-to-Recipient)
2. Direct parse_agent_message() -- NO E2E Layer

The earlier claim that "Contact Queue E2E works" was a misunderstanding. Contact Queue only has server-level decryption (Layer 1). There is no separate E2E layer.

Reply Queue: Two Layers

Incoming MSG on Reply Queue:
1. SMP Transport decrypt (Server-to-Recipient) -- Layer 1
2. ClientMsgEnvelope parse + E2E decrypt -- Layer 2
3. parse_agent_message()

Decrypted Content (15904 bytes)

[0]     0x3a ':' -- PrivHeader type (not PHConfirmation='K', not PHEmpty='_')
[2-14] Ed25519 SPKI (OID 1.3.101.112)
[...] 00 07 43 -- Agent Version 7, 'C' = AgentConfirmation
[...] EncRatchetMessage (Double Ratchet payload)

PrivHeader ':' (0x3a) is a new type not previously encountered. The content contains an AgentConfirmation with an EncRatchetMessage inside, which requires Double Ratchet decryption in the next session.


Complete Decryption Chain Status

LayerComponentStatus
0TLS 1.3Working (Session 1)
1SMP Transport (rcvDhSecret + cbNonce)Working (Session 9)
2E2E (e2eDhSecret + cmNonce)Working (Session 18)
3AgentMsgEnvelope parsingNext step
4Double Ratchet (EncRatchetMessage)After Layer 3
5Application data (ConnInfo)After Layer 4

Bug #18 Timeline

SessionDateDiscovery
12Jan 30Two separate X25519 keypairs
13Jan 30HSalsa20 difference, MAC position
14Jan 31-Feb 1DH secret verified with Python
15Feb 1maybe_e2e=Nothing, missing key theory (wrong)
16Feb 1-3Custom XSalsa20, theory disproven
17Feb 4Key consistency verified, length prefix fix
18Feb 5Root cause: 102 bytes SMP padding. One-line fix.

Seven sessions, approximately 30 sub-issues investigated, multiple theories disproven. Root cause: one line of code, 102 bytes of padding included in envelope length.


Evgeny Quotes Reference (Sessions 7-17)

DateQuoteContext
Jan 28"most likely A" (peer_ephemeral + our rcv_dh_private)Reply Queue key
Jan 28"sender's public DH key sent in confirmation header -- outside of AgentConnInfoReply but in the same message"Key location
Jan 28"TWO separate crypto_box decryption layers...different keys and different nonces"Two layers
Jan 28"it does seem like you're indeed missing server to client encryption layer"Missing layer
Jan 28"I think the key would be in PHConfirmation, no?"Key hint
Jan 26"A_MESSAGE...lacks a particular context though"Error context
Jan 26"claude is surprisingly good at analysing our codebase" / "Opus 4.5 specifically"Tool recommendation
Jan 26"make an automatic test that tests it against haskell implementation"Testing advice
Jan 26"what you did is impressive" / "first third-party SMP implementation"Recognition

Part 15 - Session 18: Bug #18 Solved SimpleGo Protocol Analysis Original date: February 5, 2026 Rewritten: March 4, 2026 (v2) One-line fix, 102 bytes SMP padding, E2E Layer 2 success