From fcb54e2b40a61d2d1a902a18e8d57fb80d5bc196 Mon Sep 17 00:00:00 2001 From: randogoth Date: Wed, 14 Oct 2026 22:08:55 +0300 Subject: [PATCH] fix: trim the body before sealing instead of appending a newline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The body was right-trimmed and then given a trailing newline back, which is reasonable for a mail body and fatal for a published twtxt record: smolboid refuses a post whose text carries a line break, because a record is one line of a feed. Trimming belongs here, before the seal, so the signature covers exactly the text the author meant. A receiver cannot do it — stripping is not invertible, so it could no longer rebuild the signed bytes to verify them. --- web/js/app.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/web/js/app.js b/web/js/app.js index a227f46..475f54f 100644 --- a/web/js/app.js +++ b/web/js/app.js @@ -950,8 +950,14 @@ async function sendMail(toText, subject, body, replyTo, anonymous) { // first reply from them reaches our main tier. const accepted = store.accepted(addr.short); if (accepted?.active) fields.push(["Accept", b32encode(tokenFor(master, accepted.identity))]); + // Trimmed before the payload is signed, so the signature covers exactly the + // text the author meant. A trailing newline is not content — but it becomes + // content the moment it is signed, and nothing downstream can strip it + // without invalidating the signature. smolboid, for one, refuses a post + // whose text carries a line break, because a published record is one line + // of a feed. const bodyBytes = utf8Bytes(buildFrontmatter(fields.filter(([, v]) => v), - body.replace(/\s+$/, "") + "\n")); + body.trim())); const envelope = seal(me, recipient, bodyBytes); // §5.8: our token for their mailbox, if they have given us one. const held = store.tokenFrom(addr.short);