• docs/superpowers/specs/2026-07-31-hashed-password-storage-design.md

    From Rob Swindell (on Debian Linux)@VERT to Git commit to main/sbbs/master on Sunday, August 16, 2026 22:55:13
    https://gitlab.synchro.net/main/sbbs/-/commit/3899ab7cae4222373328a5ca
    Modified Files:
    docs/superpowers/specs/2026-07-31-hashed-password-storage-design.md
    Log Message:
    docs: webv4 breaks under hashed passwords, in two ways

    The cleartext-sink list did not reach the web UIs, which is where the
    most visible breakage is.

    The page-content half is the RLogin auto-login convention: the password
    travels in the client-user-name field, compared against useron.pass at answer.cpp:170, so webv4/pages/003-games.xjs:64 writes
    user.security.password straight into the served HTML as Options.RLoginClientUsername. It is rendered into JavaScript source, sent
    to the browser, and sent back through the WebSocket-to-RLogin proxy when
    a door is launched. web/root/members/externals.ssjs:60 does the same in
    the v3 UI, web/lib/leftnav_html.ssjs:8 builds ftp://alias:password@host
    into the left nav of every page, and rlogin.js:86,90 is the door-side
    form -- its -h variant is no escape, since sha1(password + ...) still has
    to read the cleartext to compute it.

    The larger half is session resumption. validateSession() (webv4/lib/auth.js:106) re-establishes a cookie-carried session by
    reading the stored password back out and passing it to login(); guest
    login (:229) does the same with the Guest account's password. No pass.tab
    value makes either succeed, so under hashing every webv4 session fails on
    the request after the form login and the guest path fails before anyone
    logs in. Both go in the sinks table, in the key-or-token table, and in a
    prose block splitting the two failures; the answer to both is a
    short-lived single-use token bound to user number and client IP, accepted
    in the client-user-name slot, with the session record's existing key made
    the authenticated object.

    That also corrects a claim in Constraints. webv4's form login runs the
    KDF once, but the cookie path re-authenticates through login() on every request, so as written it costs a derivation per request exactly as HTTP
    Basic does -- making the session the authenticated object is a cost fix
    there as well as a correctness fix under hashing.

    Sequencing step 4 gains the note that neither webv4 site has a
    compare-only form, so both are their own work and must land before the
    storage change rather than folding into the mechanical verify_password() conversion.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net