From 16854e8b24a3b00d9d5dbd285bbe3b93ee47ce1c Mon Sep 17 00:00:00 2001 From: Gab Virebent Date: Mon, 6 Jul 2026 16:34:47 +0200 Subject: Guard key material in RAM with memguard, add Markdown preview for message field Reader now keeps the private key bytes and the per-submission ECDH/HKDF secrets in memguard.LockedBuffer (mlock + guaranteed wipe) instead of a manual zero loop. Added a regression test for the decrypt path. Message field gets a self-hosted Markdown toggle preview (no external library): headers, bold/italic, inline code, code blocks, blockquotes, lists, links. Output is HTML-escaped and link schemes are whitelisted before rendering. Submitted content is still the raw Markdown source, unchanged on the wire. --- static/markdown.js | 149 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 static/markdown.js (limited to 'static/markdown.js') diff --git a/static/markdown.js b/static/markdown.js new file mode 100644 index 0000000..bad1a6d --- /dev/null +++ b/static/markdown.js @@ -0,0 +1,149 @@ +// NymDrop — minimal self-hosted Markdown preview for the message field. +// No external library: a small, auditable subset is enough for a +// submission form, and it avoids pulling a third-party JS dependency into +// a page whose whole point is minimizing trust in client-side code. +// +// The rendered HTML never leaves the browser — the encrypted submission +// still carries the raw Markdown source text untouched (see app.js). This +// is purely a "what will this look like" aid for the person writing it. + +function mdEscapeHtml(str) { + return str + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +// Only allow link schemes that can't execute script in the rendered preview. +function mdSafeHref(url) { + if (/^(https?:|mailto:)/i.test(url)) return mdEscapeHtml(url); + return "#"; +} + +function mdInline(text) { + let out = mdEscapeHtml(text); + out = out.replace(/`([^`]+)`/g, "$1"); + out = out.replace(/\*\*([^*]+)\*\*|__([^_]+)__/g, (m, a, b) => `${a || b}`); + out = out.replace(/\*([^*]+)\*|_([^_]+)_/g, (m, a, b) => `${a || b}`); + out = out.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (m, label, url) => + `${label}`); + return out; +} + +function renderMarkdown(source) { + const lines = source.replace(/\r\n/g, "\n").split("\n"); + const html = []; + let i = 0; + let para = []; + + function flushPara() { + if (para.length) { + html.push("

" + para.map(mdInline).join("
") + "

"); + para = []; + } + } + + while (i < lines.length) { + const line = lines[i]; + + if (/^```/.test(line)) { + flushPara(); + const code = []; + i++; + while (i < lines.length && !/^```/.test(lines[i])) { + code.push(lines[i]); + i++; + } + html.push("
" + mdEscapeHtml(code.join("\n")) + "
"); + i++; + continue; + } + + const heading = line.match(/^(#{1,3})\s+(.*)$/); + if (heading) { + flushPara(); + const level = heading[1].length; + html.push(`${mdInline(heading[2])}`); + i++; + continue; + } + + if (/^(---|\*\*\*)\s*$/.test(line)) { + flushPara(); + html.push("
"); + i++; + continue; + } + + if (/^>\s?/.test(line)) { + flushPara(); + const quote = []; + while (i < lines.length && /^>\s?/.test(lines[i])) { + quote.push(lines[i].replace(/^>\s?/, "")); + i++; + } + html.push("
" + renderMarkdown(quote.join("\n")) + "
"); + continue; + } + + if (/^[-*]\s+/.test(line)) { + flushPara(); + const items = []; + while (i < lines.length && /^[-*]\s+/.test(lines[i])) { + items.push(`
  • ${mdInline(lines[i].replace(/^[-*]\s+/, ""))}
  • `); + i++; + } + html.push(""); + continue; + } + + if (/^\d+\.\s+/.test(line)) { + flushPara(); + const items = []; + while (i < lines.length && /^\d+\.\s+/.test(lines[i])) { + items.push(`
  • ${mdInline(lines[i].replace(/^\d+\.\s+/, ""))}
  • `); + i++; + } + html.push("
      " + items.join("") + "
    "); + continue; + } + + if (line.trim() === "") { + flushPara(); + i++; + continue; + } + + para.push(line); + i++; + } + flushPara(); + + return html.join("\n"); +} + +(function() { + const textarea = document.getElementById("message"); + const preview = document.getElementById("message-preview"); + const toggle = document.getElementById("preview-toggle"); + if (!textarea || !preview || !toggle) return; + + let showingPreview = false; + + toggle.addEventListener("click", function() { + showingPreview = !showingPreview; + if (showingPreview) { + preview.innerHTML = renderMarkdown(textarea.value) || + '

    Nothing to preview yet.

    '; + textarea.hidden = true; + preview.hidden = false; + toggle.textContent = "Edit"; + } else { + preview.hidden = true; + textarea.hidden = false; + toggle.textContent = "Preview"; + } + }); +})(); -- cgit v1.2.3