Repository navigation
Wipe the session's plaintext buffers when they grow, on a decrypt failure and at the session's end #77
Description
Problem
A pairing message's plaintext can still reach the heap unwiped, or stay in the session's message buffer, outside the wipe that runs once a pairing message is parsed. A long-term PSK sent in client/pair-finalize is the secret at stake. Found while fixing #20, which wipes the buffers at parse time and on a failed fragment sequence but does not cover these paths.
Where
- Reassembly growth:
sendspin-server-cpp/src/fragmentation.cpp
Line 89 in cf233d2
this->buffer_.insert(this->buffer_.end(), data.begin(), data.end()); - The session's message buffer, its growth and the decrypt failure:
sendspin-server-cpp/src/session.cpp
Lines 208 to 212 in cf233d2
this->scratch_.assign(payload.begin(), payload.end()); if (!this->ciphers_ || !this->ciphers_->decrypt(this->scratch_)) { this->finish(SessionEnd::DECRYPT_FAILED); return; } - In-place decrypt:
sendspin-server-cpp/src/noise.cpp
Line 115 in cf233d2
bool TransportCiphers::decrypt(std::vector<uint8_t>& message) { - The wiping allocator ArduinoJson already uses:
sendspin-server-cpp/src/messages.cpp
Line 37 in cf233d2
class WipingAllocator : public ArduinoJson::Allocator {
Evidence
Traced, not reproduced:
- (a) Reallocation:
Reassembler::accept()grows its buffer withinsert(), andSession::scratch_.assign()grows past its capacity. Each frees the old block holding the bytes so far, unwiped, so earlier fragments of a pairing message go back to the heap. - (b) Session end:
Sessionhas no destructor andfinish()does not wipescratch_. A session that ends (close, timeout, any other end) while a pairing message's fragment is pending freesscratch_holding that fragment's plaintext. - (c) Decrypt failure: on the host's OpenSSL AES-GCM backend,
TransportCiphers::decrypt()writes the plaintext in place before the tag check. A genuine client/pair-finalize whose tag was altered on the path leaves itslong_term_pskinscratch_when the session ends with DECRYPT_FAILED. ChaChaPoly checks the tag first, and the ESP-IDF GCM path zeroes its output on failure.
Direction
Either give scratch_ and the reassembly buffer an allocator that wipes on deallocate (as WipingAllocator does for ArduinoJson), or wipe scratch_ at the session's end and on a decrypt failure, and grow both buffers by hand, wiping the old block.
Done when
- A test shows no PSK bytes in
scratch_after an AES-GCM pair-finalize whose tag fails. - A test shows no PSK bytes left when a session ends with a pairing message's fragment pending.
- Growing either buffer leaves no unwiped copy of its earlier contents.
Activity
Wipe a pairing message's plaintext even when it fails validation (#78)
kahrendt commentedon Oct 15, 2026
Claimed 3349b831; branch issue-77-plaintext-wipes
kahrendt commentedon Oct 15, 2026
Draft PR: #89