• 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 Sat Aug 1 17:58:53 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/814c46c6b12043d4ce293d58
    Modified Files:
    docs/superpowers/specs/2026-07-31-hashed-password-storage-design.md
    Log Message:
    docs: weigh the two storage options against each other honestly

    This pass is about the comparison the document was failing to make, and
    about a claim that made it look like there was nothing to compare.

    "Why the digest authentication schemes must go" was stated
    unconditionally. It opened by observing that a server verifying an HMAC
    keyed by the password must hold something password-equivalent, and that
    no storage format changes this. Encryption changes it exactly: it holds
    the password and can recover it, which is the reversibility the whole
    trade is about. The section therefore read as though the deprecations
    followed from moving credentials out of user.tab, when they are the price
    of the hashing branch alone. It is now titled and framed as conditional,
    and its table column says "fate under hashing".

    The two branches were also argued asymmetrically. Encryption's drawbacks appeared as a list where the choice is posed; hashing's -- the digest deprecations, password-by-email, retroactive auditing, client
    reconfiguration, the transport regression -- were scattered across four
    later sections, so a reader had one column in front of them and had to
    assemble the other. A side-by-side ledger now sits at the decision point, covering the three properties, the failure mode, each capability that
    dies, client reconfiguration, transport, key management and the portable
    floor. Neither column is empty, and the choice is not deducible from it: hashing's costs are certain, immediate and land on identifiable users,
    while encryption's is a low-probability event with an unbounded blast
    radius.

    That weighing is now recorded rather than left implicit, as a judgment
    and not a measurement. At-rest exposure is both the more likely and the
    more severe threat: it is present on every system since each account was created and in every backup, where interception requires an attacker
    positioned on a path, and the realistic adversary for a hobby BBS is
    commodity infostealer malware scraping files rather than anyone on the
    wire. One interception yields one credential for one session; one copy of user.tab yields every account retroactively, with no detection and no
    expiry. The protection being defended is narrower than it looks, besides
    -- CRAM-MD5 and APOP cover the mail protocols, while terminal login, FTP
    and HTTP Basic send the password in the clear today and are untouched by
    them (login.cpp:102, ftpsrvr.cpp:2684, websrvr.cpp:2142).

    Two qualifications are recorded as insufficient to reverse it: an
    intercepted CRAM-MD5 exchange is an unsalted MD5 HMAC over a known
    challenge and so is offline-crackable, and the affected users currently
    have both exposures. Two things follow. A sysop confident in their
    storage whose users are on hostile networks has a real argument for
    encryption. And refusing plaintext authentication on unencrypted
    connections is not negotiable, since silently moving CRAM-MD5 users to
    PLAIN on port 110 is the worst outcome available and the default if
    nobody decides otherwise.

    Preceding that, two claims defending the deprecations are withdrawn.
    Hashed storage "needs no user action" only where nothing about the login changes -- terminal, FTP, HTTP Basic, and clients already sending plain credentials over TLS -- and not for anyone who must reconfigure a mail
    client. And a server-side replacement is not a replacement unless the
    client can reach it: a POP3 client with CRAM-MD5 and no TLS sends a
    digest today and never transmits the password, but after removal either
    stops working or sends it in cleartext. That is a transport-security loss
    for a subset in exchange for an at-rest gain for all, and calling the server-side work a "replacement path" hid that it is a trade. SCRAM does
    not rescue that population either, since the clients in question do not implement it.

    Finally, the key-storage section stopped treating a preinstalled package
    as an operating-system capability. It listed systemd-creds as what
    "Linux" offers and the BSDs as having "nothing standard", comparing a
    package that happens to be preinstalled against one that happens not to
    be. Only DPAPI and the macOS Keychain are base-system facilities;
    systemd-creds is systemd, which Alpine, Void, Devuan and OpenRC systems
    do not have, and TPM2 sealing is hardware plus tpm2-tss, a package or
    port everywhere including the BSDs. The axis is default availability, not standardness, and the real capability question is whether the machine has
    a TPM -- which cuts across OS families. The table now records where each facility comes from, adds the row that was missing (a mode-0600 file, the floor, which works everywhere), and the design names none of them:
    Synchronet reads the key from a location the sysop configures, and how
    that location is protected is their platform's business.

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

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian Linux)@VERT to Git commit to main/sbbs/master on Sat Aug 1 18:28:45 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/84f2303f31ab3b172080d525
    Modified Files:
    docs/superpowers/specs/2026-07-31-hashed-password-storage-design.md
    Log Message:
    docs: cite the Gmail precedent, and correct two overstated costs

    Three corrections, all of which make the hashing branch cheaper than the document claimed.

    The question that started this analysis was how large providers support
    digest authentication without holding cleartext passwords. The answer --
    that they do not support it -- was never recorded. Gmail's IMAP, POP3 and
    SMTP servers offer XOAUTH2 and credentials over TLS; CRAM-MD5, DIGEST-MD5
    and APOP are absent. Faced with the same theorem this document derives,
    the largest mail provider in the world dropped the mechanisms rather than
    keep the passwords recoverable. Their fallback for clients that cannot do
    OAuth is the same shape recommended here for MQTT: an app password is a generated 16-character credential, scoped to one application or device, revocable individually, and invalidated when the account password
    changes. Two limits are recorded with it -- Google can require 2-Step Verification and can afford to break non-complying clients, and their
    scale makes an at-rest breach a different kind of event -- so the
    precedent is cited for the choice, not for the weighing behind it.

    The TLS mail listeners already ship, so the transport prerequisite
    shrinks from building to enabling. ctrl/services.ini defines [IMAPS] on
    993 with Options=TLS, and ctrl/sbbs.ini defines TLSSubmissionPort 465 and TLSPOP3Port 995. What remains is to enable and document them, since both
    IMAP services ship Enabled=false, and to decide what the cleartext ports
    do afterwards. STARTTLS on 143 still does not exist and is optional if
    993 is the answer. None of this changes the separate point that a
    server-side port is not a replacement unless the client can use it.

    And the key-storage fallback was described as "a mode-0600 file" and
    treated as the floor, as though the only alternative to a TPM were a
    secret sitting in the open. There is a wide range in between, most of it portable and cheap. The rungs are not degrees of cryptographic strength
    but a question of which adversary each defeats, and the three that matter
    are different people: someone holding a backup, something else running as
    the BBS user, and someone with the machine.

    The two free rungs are the largest steps available. Having the supervisor
    hand the key over keeps it out of the filesystem the BBS user can read at
    all -- systemd's LoadCredential= has systemd read the key as root and
    place it in a tmpfs readable only by that service, and Windows has the
    same shape through a service account and an ACL. This does not stop a
    live compromise of the process holding the key, which holds it by
    definition, but it stops offline reading: a backup, a stolen disk, a file-disclosure bug, or any other process running as sbbs. Keeping the
    key outside the installation tree aims at this document's threat model directly, since backups take the BBS directory. Above those: a passphrase-protected keyset, which this tree already uses for the TLS
    private key (ssl.c:480-482); a machine-bound value mixed in, so a backup restored onto other hardware is useless; mlock(); then the OS facilities
    and TPM sealing.

    Two corrections of scope come with that. Unattended startup does not
    conflict with hardware-backed protection, since a TPM is read by the
    machine and not by a person, so the constraint recorded earlier rules out
    only the passphrase-at-startup variant. And the no-TPM case is narrower
    than claimed: Windows 11 requires TPM 2.0, most x86 hardware since
    roughly 2016 ships a firmware TPM, and macOS has the Secure Enclave
    behind the Keychain. The genuine gaps are single-board computers, small
    VPS instances with no virtual TPM, older hardware, and firmware TPMs left disabled -- host types BBSes do run on, so the fallback is not
    hypothetical, but a minority rather than the default.

    Finally, since a new or additional crypto library is likely part of this
    work, the selection criteria are extended from "which KDF" to four: a
    modern password KDF, authenticated key wrapping, memory-hygiene
    primitives, and a PKCS#11 or provider path that makes hardware a
    configuration choice rather than per-platform code. libsodium and OpenSSL
    are added as candidates and scored against those. The KDF is revisable
    later because records are self-describing; the key-protection facilities
    are not, which is why they belong in the decision rather than after it.

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

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian Linux)@VERT to Git commit to main/sbbs/master on Sat Aug 1 19:37:26 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/b0cd835deedbfbf2f621d739
    Modified Files:
    docs/superpowers/specs/2026-07-31-hashed-password-storage-design.md
    Log Message:
    docs: pin down the Windows versions behind the secret-storage options

    The Windows rows in the key-storage table were flagged as unverified.
    They are now checked against the API documentation and carry their
    minimum supported versions: CryptProtectData and CredWriteW are Windows
    XP and Server 2003, and NCryptProtectSecret is Windows 8 and Server 2012, desktop apps only.

    The practical answer is that this is not a version question. The MSVC
    project targets the Windows 10 SDK with toolset v143, so all three are available on any Windows a current Synchronet build runs on, and choosing between them turns on their properties rather than on availability.

    One caveat is recorded as not applying: DPAPI's interactive CRYPTPROTECT_PROMPTSTRUCT flow is deprecated for removal in February
    2027, but an unattended service passes NULL and takes the non-interactive
    path.

    Also drops an unverified claim that Credential Manager's per-item blob
    limit is "a few kilobytes". The limit is small and the figure was not confirmed; what matters is that it is ample for one key and useless in
    bulk, which is what the row now says.

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

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian Linux)@VERT to Git commit to main/sbbs/master on Sat Aug 1 21:07:52 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/6c46551d19c36b252e655abd
    Modified Files:
    docs/superpowers/specs/2026-07-31-hashed-password-storage-design.md
    Log Message:
    docs: the crypto abstraction and its libraries are already in the tree

    The crypto-library section posed the choice as one to be made from
    scratch. It is largely made. src/xptls/ provides xp_crypt, a portable abstraction with Botan 3, OpenSSL and null backends whose outputs are byte-compatible by design, and xp_crypt.h:66 already declares a KDF
    interface with PBKDF2-HMAC-SHA256, scrypt, and a placeholder comment
    naming Argon2id as gated on OpenSSL 3.2+ or Botan support.

    The libraries are in place too: Botan 3 is vendored as
    3rdp/dist/Botan.tar.xz and built only when the probe finds neither
    system Botan 3.6+ nor OpenSSL 3.0+ (3rdp/build/GNUmakefile:61-70), so
    every build already resolves to one of the two. Neither is a new
    dependency to argue for, and a vendored Argon2 is redundant if Argon2id
    reaches xp_crypt through either backend.

    What actually remains is narrower: add Argon2id or accept scrypt; link
    xp_crypt where the user database is touched, since it is consumed by
    SyncTERM and the SSH code but not by sbbs3, which leaves the sbbsecho and makeuser linkage as the real work; and decide what covers key wrapping,
    memory hygiene and a hardware path, none of which xp_crypt does today.
    The candidate list is rewritten around the two backends that exist, with libsodium's objection restated as "a third crypto library in a tree that carries two" rather than any technical shortcoming.

    Three smaller points. The threat model does not depend on sysops keeping backups, and would survive the observation that many do not: the BBS
    makes the copies itself, daily, without being asked, which is where the
    seven cleartext copies on the surveyed system came from -- none of them a deliberate backup.

    How rare the no-TPM case is remains contested, and the counter-view is recorded: that essentially every sysop has a TPM and the Raspberry-Pi-and-stranger-hardware population is a special case rather
    than a design constraint, which would make the file fallback a
    compatibility path rather than something the design is shaped around.

    And the invisibility of the storage choice to users is recorded as not a
    gap to be filled, with no protocol signal to be invented for it. A user
    who cared already uses a distinct password per system. The design exists
    for the ones who do not and will never think about it, and telling them
    which BBS hashes would change nothing about what they type.

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

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian Linux)@VERT to Git commit to main/sbbs/master on Sun Aug 16 22:55:13 2026
    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