Files
SRCmail/vnc/VNC-CHANGES.md
T
Bernd RodlerandClaude Opus 5 b6fdfe72ca chore: housekeeping — rescue orphaned doc, ignore .DS_Store, adopt vnc-v0.3.0
Commits the offline-client architecture analysis doc that was sitting
untracked in docs/ — its own header already warns this exact thing
happened once before (~/vncmail-plus is a shared checkout; an earlier
untracked copy was lost to a concurrent branch switch). Confirmed the
hazard is still live: vnc/VNC-CHANGES.md itself was found deleted from
disk mid-edit by this session, by something else touching the checkout
concurrently, and had to be restored with `git checkout --` before this
commit. Committing on sight is the only defense against that, not a
process improvement for later.

Also:
- .DS_Store added to .gitignore (was untracked in docs/)
- introduces a VNC-side feature version, separate from package.json's
  upstream-tracking version (1.7.8, must stay that way per the fork's own
  rule 4 - bumping it would turn merging upstream releases into a diffing
  exercise). Retroactively bucketed at the milestone boundaries the commit
  history already has: v0.1.0 fork bootstrap, v0.2.0 S/MIME plugin
  audit+fixes, v0.3.0 the internal-CA foundation just landed. Tagged
  vnc-v0.3.0 on this commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 13:10:03 +02:00

10 KiB

VNC-CHANGES — VNCmail+ divergence log

VNCmail+ is a fork of bulwarkmail/webmail (AGPL-3.0). This file records every intentional divergence from upstream so that merging new upstream releases stays a triage exercise, not an archaeology dig.

VNC feature version

package.json's version tracks upstream (currently 1.7.8) and must stay that way per rule 4 below — bumping it would turn merging upstream releases into a diffing exercise instead of a fast-forward. The VNC-side feature set gets its own counter instead, tagged vnc-vX.Y.Z on dev, bumped whenever a milestone below closes:

Version Date Milestone
v0.1.0 2026-08-03 Fork bootstrap: VNClagoon + SRC brand themes, per-theme logos, k8s deploy (after Vercel was abandoned — Bulwark writes to a local data dir, serverless fs is read-only), 6h session cookie
v0.2.0 2026-08-04 SRC theme MD3 componentry; plugin-sandbox hardening (B-01 scanner bypass, B-04 unpermissioned hook registration); S/MIME plugin forked, audited (S-01, 9 findings), 3 shipping-blockers fixed + 2 hardened, verified end-to-end on real mail (sign, encrypt, decrypt, banner)
v0.3.0 2026-08-04 Internal CA foundation (P1): EJBCA Community manifests + root-ceremony runbook (A-01/A-06), server-side CaProvider + enrolment route (A-02, C-08 server half), finding 11 (certificate address binding). No certificate has been issued yet — the browser half of C-08 (in-browser CSR generation) and a live EJBCA are both still outstanding.

Rules of the fork

  1. Keep upstream files unmodified whenever possible. Prefer env vars (branding), the vnc/overrides/ layer, and additive files over editing files inside app/, components/, lib/, stores/.
  2. Every edit to an upstream file gets a one-line entry below — path, what, why. No silent edits.
  3. Branches: main tracks upstream releases (kept clean); dev is the VNC integration + Vercel deploy branch; vnc/* are feature branches.
  4. Syncing upstream: git fetch upstream && git merge upstream/main onto main, then merge main into dev. Resolve using this log.

Divergences

Date File / area Change Why
2026-08-03 vnc/ (new) Added VNC customization dir + this log Fork bootstrap
2026-08-03 VNCMAIL-SETUP.md (new) Vercel deploy runbook Deploy on Vercel as project "VNCmail+"

| 2026-08-03 | deploy/k8s/ (new) | k8s manifests + runbook for microk8s deploy | Bulwark is stateful → runs as a container w/ persistent volumes, not Vercel serverless | | 2026-08-03 | .gitignore | ignore deploy/k8s/secret.yaml | keep the real env secret out of git | | 2026-08-03 | public/branding/*.svg (new) | VNCmail wordmark (on-dark + on-light) | VNC logo (placeholder — swap official SVG) | | 2026-08-03 | public/fonts/*.woff2 (new) | DM Sans 400/500/700 + Syne 700/800 (OFL, self-hosted) | VNClagoon typography | | 2026-08-03 | lib/builtin-themes.ts | add builtin-vnclagoon theme (navy+cyan, DM Sans/Syne, @font-face) | VNClagoon brand theme | | 2026-08-03 | lib/admin/types.ts | DEFAULT_THEME_POLICY.defaultThemeIdbuiltin-vnclagoon | make VNClagoon the default theme | | 2026-08-03 | stores/theme-store.ts | default theme/resolvedThemedark | dark-first per VNClagoon styleguide | | 2026-08-03 | lib/builtin-themes.ts | add builtin-src theme (Swiss red on white, light-first) | 2nd brand theme (SRC Advisory); VNClagoon stays default | | 2026-08-03 | public/branding/src-logo.svg (new) | SRC mountain mark | SRC brand (placeholder — swap official) | | 2026-08-03 | lib/plugin-types.ts | add optional logoLightUrl/logoDarkUrl to InstalledTheme | per-theme brand logos | | 2026-08-03 | lib/theme-logo.ts (new) | resolveThemeLogo() helper | pick active theme's logo, fall back to global | | 2026-08-03 | lib/builtin-themes.ts | set logos on vnclagoon (wordmark) + src (mark) | logo switches with the brand theme | | 2026-08-03 | app/(main)/[locale]/login/page.tsx | login logo uses active theme's logo | brand-switch on login | | 2026-08-03 | components/layout/navigation-rail.tsx | nav-rail logo uses active theme's logo | brand-switch in app |

Note: Vercel was tried and abandoned on 2026-08-03. Bulwark writes to a local data dir (/app/data/*); Vercel serverless has a read-only filesystem → crash (ENOENT /var/task/data). VNCmail+ now deploys as a Docker image (ghcr.io/brvncde-dotcom/vncmail-plus-*) on Kubernetes (microk8s) at vncmail.sandbox.vnc.de, with 4 persistent volumes — see deploy/k8s/. A microfrontends integration was also added and reverted the same day.

| 2026-08-03 | lib/stalwart/auth-context.ts | give jmap_stalwart_ctx a 6-hour maxAge (was session-cookie → expired on tab close) | session survival across browser restarts | | 2026-08-03 | lib/builtin-themes.ts | add srcSkin (MD3 component overrides: shape scale, filled buttons, text fields, cards, dialogs, state layers, switches, login card); add @font-face + typography to builtin-src; bump to v1.1.0 | SRC theme: keep colors + fonts, apply MD3 design system | | 2026-08-04 | lib/plugin-sandbox/loader.ts | B-04 security fix — gate hook registration on granted permissions via new HOOK_PERMISSIONS map; refused hooks are skipped, logged and counted | info.hooks is self-reported by the sandbox, so an untrusted plugin could claim onRenderEmailBody and replace any rendered email body without holding email:render-takeover. Consent copy gated what the user was asked, not what the host allowed. | | 2026-08-04 | lib/plugin-sandbox/host-api.ts | export hasPermission() (was module-private) | one source of truth for the permission rule — the loader gate and the RPC gate must not drift apart | | 2026-08-04 | app/api/admin/plugins/route.ts | B-01 — scan all .js/.mjs in the bundle (was entrypoint only); return structured findings + canOverride; allow admin overrideWarnings=true with a plugin.install.scan_override audit entry; echo accepted findings on success | hard-reject on eval(/new Function(/innerHTML = made every crypto plugin uninstallable (minified openpgp.js/pkijs trip it), while only scanning the entrypoint left a trivial bypass. Route is already admin-authenticated, so the scan is defence-in-depth, not a trust boundary. | | 2026-08-04 | vnc/audits/SMIME-PLUGIN-AUDIT-2026-08-04.md (new) | S-01 security audit of bulwarkmail/plugins/smime @ 91085a3 — 9 findings (2 HIGH, 1 MED-HIGH), verdict: fork and fix before shipping | privileged same-origin plugin that handles users' private keys; verdict must precede any deploy | | 2026-08-04 | vnc/plugins/smime/ (new) | fork of the upstream S/MIME plugin, source only — upstream smime.zip deliberately NOT vendored | shipped zip is a 1.77 MB bundle at manifest 1.0.1 while source is 1.0.2, so auditing src/ would not audit what the zip installs. We build from source via npm run package. | | 2026-08-04 | vnc/plugins/smime/src/index.js | audit fix 1 (HIGH)maybeAutoImportSigner now requires signerEmailMatch === true and !selfSigned before trusting a signer cert | upstream gated on signatureValid alone, but smimeVerify runs checkChain:false, so a self-signed cert asserting any address was silently stored as the ENCRYPTION TARGET for it. Both values were already computed and ignored. | | 2026-08-04 | vnc/plugins/smime/src/mime-builder.js | audit fix 3 (MED-HIGH)stripCrlf() applied inside formatHeader + the 3 directly-assembled headers (att.contentType, att.cid, smimeType) | CRLF escaping reached only Subject and filename; display names, Message-ID, In-Reply-To and References were raw — and those are copied from inbound mail on reply/forward, making it remotely reachable header injection | | 2026-08-04 | vnc/plugins/smime/src/smime-decrypt.js | audit fix 2 (HIGH) — content-encryption allowlist (AES-CBC + AES-GCM only, gate runs before any key use); normal decrypt moved to nativeEngine() so the liner engine is reachable only for a genuine legacy RSAES-PKCS1-v1_5 key; return contentAuthenticated | upstream applied NO algorithm check and ran every decrypt through the liner engine, which registers DES-CBC/3DES-CBC/RC2-CBC for PKCS#12 password encryption — the CMS path inherited them. AEAD-only would break interop (RFC 5751 mandates AES-128-CBC), so CBC stays and the weak ciphers go. | | 2026-08-04 | vnc/plugins/smime/src/index.js | audit fix 2 (cont.) — suppress HTML when content is unauthenticated (CBC), text-only, behind new renderUnauthenticatedHtml setting (default false) | CMS EnvelopedData has no MAC, so CBC plaintext is malleable and HTML rendering is EFAIL's exfiltration channel. The host blocks remote content by default but that's a setting the plugin can't observe — don't lean on it. Our own encrypt path is always AES-GCM, so outbound mail renders fully. | | 2026-08-04 | .gitignore | ignore vnc/plugins/smime/{node_modules,dist,smime-vnc.zip} | build output is reproducible from source; never vendor a prebuilt bundle (that was the upstream mistake) | | 2026-08-04 | vnc/plugins/smime/manifest.json | add auth:observe | plugin registers onAfterLogout/onAccountSwitch (real hooks, lib/plugin-hooks.ts:362-363) without declaring the permission; under B-09 the session-key wipe would silently stop running | | 2026-08-04 | vnc/plugins/smime/verify-fixes.mjs (new) | 36 regression assertions across all three fixes, incl. source checks that fail if a guard is removed, a legacy CBC OID reappears, or the mail path stops using the native engine | the source assertion caught an interpolated header manual review had wrongly dismissed as static | | 2026-08-04 | vnc/plugins/smime/roundtrip.mjs (new) | real crypto round trip through the plugin's own modules — PKCS#12 import → unlock → sign → verify → encrypt → decrypt, 24 assertions, no browser required | proves the three audit fixes did not break S/MIME. Run: node vnc/plugins/smime/roundtrip.mjs <certdir> | | 2026-08-04 | app/(main)/admin/_tabs/plugins.tsx | B-01 (UI) — scanner-findings review panel: holds the rejected file, lists pattern-per-file, offers "Install anyway" / "Cancel"; success message reports how many findings were accepted | without this the override was API-only — an admin uploading a crypto bundle through the web form hit a 400 they could not act on. Also replaces a dead data.warnings read (never returned by the route) with the live findings field. |

(append new rows as you diverge)