• 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 00:24:30 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/83ce42da5c560cfdb39cd599
    Added Files:
    docs/superpowers/specs/2026-07-31-hashed-password-storage-design.md
    Log Message:
    docs: design for hashed password storage in data/user/pass.tab

    Records the evaluation of moving Synchronet off cleartext password
    storage, against a specific threat model: someone abusing a copy of
    user.tab -- including a sysop or co-sysop -- to recover passwords that
    users have reused on other systems.

    Reversible encryption is rejected outright. The named adversary holds
    the key by definition, since every server process must be able to
    recover the password to serve a login. Only a slow, salted, one-way
    hash addresses the goal, which in turn requires deprecating the challenge-response mechanisms that need password-equivalent material at
    rest: CRAM-MD5, APOP, HTTP Digest, Hotline, and the MQTT broker PSK
    table.

    Per-mechanism verifiers -- storing the two MD5 midstates so CRAM-MD5
    keeps working, as Dovecot does -- are considered and rejected. The
    verifier is a single unsalted MD5 compression, so storing several
    verifiers leaves you with the security of the weakest one. SCRAM
    (RFC 5802/7677) is recorded as the correct successor, but is not a
    prerequisite for this design.

    Credentials move to a new data/user/pass.tab rather than growing
    LEN_PASS. user.tab is the artifact that actually circulates, and its
    1000-byte record geometry has roughly 15 bytes of headroom for a maximally-populated record. The USER_PASS field gets an unmatchable
    sentinel rather than being blanked, because login.cpp treats an empty
    password as "no password required" and the hosts sharing /sbbs do not
    upgrade atomically.

    Also covers the migration strategy, the case-sensitivity problem, the
    remaining cleartext sinks, the denial-of-service risk of running an
    expensive KDF on HTTP Basic's per-request authentication, and the
    cleartext copies left behind in the rotated user.tab backups.

    Design only; no code changes.

    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 14:45:07 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/143efec68b09ab0c43f58c4b
    Modified Files:
    docs/superpowers/specs/2026-07-31-hashed-password-storage-design.md
    Log Message:
    docs: revise the hashed-password design after review

    Restates the threat model as two testable properties, so that the rest of
    the document is measured against something falsifiable rather than
    against a job title. P1: two users with the same password must not
    produce the same stored record. P2: given the file and a candidate
    plaintext, confirming a match must not be cheap. A malicious sysop is now explicitly conceded as unmitigable by any storage design; what is
    addressed is careless handling of a database that someone else later
    obtains.

    Records the decision that hashed storage is mandatory, not a
    sysop-selectable mode. The user who benefits cannot tell a BBS that
    hashes from one that does not, so an option assigns the choice to the
    party that bears none of the consequence. That removes the per-system
    switch and makes the IMAP transport work a hard prerequisite, since a
    sysop whose mail client breaks has no way back.

    Corrects two errors. No browser has deprecated HTTP authentication;
    what is true is that RFC 7616 deprecates MD5 within the scheme while
    browser support for the SHA-256 variants is essentially nonexistent.
    And HTTP Basic cannot be moved onto session tokens -- the browser
    resends the credential on every request and the server cannot opt out --
    so the denial-of-service mitigation is a verification cache plus rate
    limiting, not a change of authentication scheme.

    Sharpens the rebuttal of per-mechanism verifiers. The technique is real
    and RFC 2104 section 4 describes it; the objection is that unsalted MD5 midstates fail P1 outright and fail P2 by orders of magnitude. The same
    RFC section stresses that those intermediates must be protected exactly
    as secret keys are.

    Completes the audit: the built-in C++ broker enumerates sysop passwords
    just as broker.js does, and qtmonitor persists a sysop password as a
    TLS-PSK key on the sysop's own workstation. Both belong to a wider
    pattern, now written up separately -- several sites use the password as
    key material or as a secret handed to third-party code, where a
    generated credential belongs. Those are pre-existing misuses that hashing merely exposes, and fixing them is worthwhile on its own schedule.

    Also: pass.tab gains a header record carrying its own record length, so
    it does not repeat user.tab's hardcoded stride; and the crypto-library
    choice moves from an open question to a constraint, because userdat.o
    links into sbbsecho and makeuser, neither of which links cryptlib.

    Design only; no code changes.

    Reviewed by Deuce.

    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 14:45:07 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/60a4e00040a6fbdf291f1ec7
    Modified Files:
    docs/superpowers/specs/2026-07-31-hashed-password-storage-design.md
    Log Message:
    docs: settle the pass.tab record format and sharpen the threat model

    Second pass over the hashed-password design, following review.

    The threat model now names two assets rather than dismissing one. An
    earlier draft said the asset being protected "is not the user's access to
    this BBS", which overcorrected: a leaked user.tab is directly usable to
    log in as any user on every server the BBS runs. Reuse elsewhere stays
    primary because the harm is unbounded and nobody here can see it happen
    or undo it; access to this BBS is secondary because it is bounded and a
    mass reset ends it, not because it is small.

    That adds a third property, P0 -- the stored record must not itself authenticate. It is free once the other two are met, but it decides the
    second asset, and a proposal can satisfy it and still achieve nothing:
    an unsalted fast hash meets P0 and fails both others. It also sharpens
    the case against per-mechanism verifiers, which fail P0 for their own mechanism, since a CRAM-MD5 midstate answers any challenge by
    construction.

    Sysop accounts already have a second factor, and the model says so. The
    system password lives outside the user database, gates sysop login when enabled, and gates sysop commands unconditionally, so a copy of user.tab
    yields the sysop's account but not the sysop's authority. It is bounded
    -- one shared secret, itself cleartext, useless for a reused password
    and for every non-sysop account -- and its storage and use stay out of
    scope.

    Encryption is rejected on better grounds than before. "A sysop holds the
    key" proves too much, so the document now answers the version that
    argument does not touch: a key held off disk with randomized ciphertext
    per record. That still fails P0, fails totally and retroactively where
    hashing degrades gracefully, and has nowhere to keep a key on a machine
    that runs unattended -- and it buys nothing hashing does not, since the
    BBS never needs to recover a password, only to answer yes or no about
    one. The malicious-sysop concession is scoped accordingly: it is a limit
    of password authentication, not of authentication, since an SSH host
    never receives the private key.

    The pass.tab format is settled. Records are tab-separated, tab-padded
    and LF-terminated as the extension claims, holding a normalization field
    and one opaque verifier. The verifier is not split into algorithm, salt
    and digest, because password libraries verify from the encoded string
    and reassembling it byte-exactly is a subtle bug for the sake of
    legibility; because bcrypt already does not fit that shape; and because
    an imported user base can never be re-hashed, so foreign verifiers must
    be storable verbatim. Normalization is its own field rather than a pbkdf2-sha512-uc pseudo-algorithm, since uppercasing is Synchronet's
    behavior and not the KDF's. One verifier per record, not a list: the
    only use for a list is the per-mechanism verifiers this design refuses.

    The record length is measured rather than stored -- read to the first LF
    and that is the stride. No header, so the offset arithmetic stays
    (N - 1) * reclen, nothing has to be written for it to work, and there is
    no second copy of the fact to go stale. It requires that no field
    contain an LF, that the measurement be range-checked against a file size
    that divides evenly, and that the compile-time default apply only when
    creating an empty file. user.tab can adopt this as a code-only change
    whenever its own record has to grow, since every existing file already
    answers 1000 to the measurement.

    The sentinel left in the vacated USER_PASS field is one byte, not forty.
    It only has to be non-empty and unenterable, and getstr() accepts
    nothing below a space, so a control byte suffices -- avoiding tab, LF,
    CR and the attribute-code 0x01. A printable sentinel would become a
    universal password on any reader that predates the change, which is the
    one scenario it exists for. Because every record carries it, the field's worst-case contribution drops from LEN_PASS to one byte, giving the
    format headroom instead of consuming it.

    Two smaller corrections. The user.tab geometry analysis is gone: it
    existed to argue against widening LEN_PASS, and once credentials moved
    to a separate file that argument had already won. And the document no
    longer argues from one installation's setup -- exported over SMB, three
    hosts sharing a mount, counts from a named system -- none of which most
    BBSes have, and none of which the reasoning needs.

    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 15:34:02 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/9f1adb69902828d5b5ed8331
    Modified Files:
    docs/superpowers/specs/2026-07-31-hashed-password-storage-design.md
    Log Message:
    docs: scope the password design honestly and narrow its argument

    Third pass, following review. Most of this is retraction.

    The case against encryption rested on four claims that are now withdrawn:
    that a sysop necessarily holds the key; that the key would sit on the
    same disk, in the same backup, as the data; that two users with the same password would encrypt to the same ciphertext; and that encryption fails
    the non-usability property by definition.

    The first three each evaluated encryption at its worst instantiation and rejected the primitive for the result, which is an argument available
    against anything -- an unsalted single-round SHA-256 is "hashing" too,
    and it would fail every property this document sets out. The fourth was circular: it read the property as "is a one-way function" and then
    observed that encryption is not one. A ciphertext record is not a
    credential; it cannot be presented to a login prompt and without the key
    it authenticates nothing.

    The property survives and keeps its job -- ruling out cleartext, and
    ruling out the CRAM-MD5 midstate, which is directly presentable as a
    credential with no key at all -- but encryption is not in that class and
    is now recorded as satisfying it. Which leaves encryption, competently
    built, meeting every property this document asks for and beating hashing
    on one: against a file-only adversary there is no offline attack at all
    without the key, where a hash only makes one expensive. A weak password survives a stolen ciphertext and does not survive a stolen hash.

    So the rejection is now narrow, and rests on two things. Encryption fails totally and retroactively when it fails, where hashing has no single
    secret whose loss exposes everyone and charges the attacker per account instead. And the BBS never needs to recover a password, only to answer
    yes or no about one, so reversibility is a capability that will never be
    used and is paid for in key management that hashing does not incur.

    The key-management cost is also stated at its true size. Every operating
    system Synchronet runs on has secret storage, and distributing a key to a multi-host installation means provisioning the same secret into each
    host, not federating their key stores -- an ordinary setup step of the
    same kind as installing a TLS private key. What remains is a manual step
    per host, repeated on rotation, with the secret outside any key store in transit. That is some cost, not much, and the failure mode is what
    actually decides the question.

    The document also now states its own scope, and the criticism of it. An analysis that answers every existing use of passwords with "passwords,
    but stored more carefully" is a narrow answer to a broad problem, and
    pairing it with the removal of working features makes the narrowness
    harder to excuse. Most of that is correct. A large share of the sites catalogued here should not use a password at all: the MQTT broker wants
    an issued API key, qtmonitor wants the key it was issued rather than a
    human's login, door dropfiles want a per-session token or nothing, and
    the human-facing surfaces want FIDO2 on the web, public keys at the
    terminal, and client certificates for mail. Each of those is a larger improvement than this design is.

    What defends proceeding is that storage is the floor rather than the
    ceiling. No post-password mechanism retires an existing password -- a BBS
    with a thousand records still has a thousand passwords the day FIDO2
    ships, and most of those users will never enrol anything. Storage is the
    only measure that requires no user action, which matters when the threat
    is a file that leaks rather than a user who chooses badly. And it is a precondition rather than a competitor, since SCRAM stores the salted
    iterated verifier this design already builds.

    Accordingly the removed mechanisms are recorded as a cost rather than as cleanup, in "What this delivers" alongside the limitations, and the rule
    that nothing is removed without a replacement path is stated generally
    rather than only for IMAP.

    FIDO2 and API keys are named for the first time, in place of gesturing at
    the families they belong to. FIDO2 is the strongest of the post-password options and the one users would plausibly adopt: the private key is not extractable even by the user, the server stores a public key, and a
    leaked credential file is inert rather than merely expensive. Being origin-bound, it addresses the reused-credential asset by construction
    instead of by cost. Its limit is transport rather than merit -- WebAuthn
    is a browser API -- which is why the post-password map is three protocols
    and not one.

    For MQTT the standard answer has a name, API key generation, and the
    lifecycle is the point rather than the value: issued to a client rather
    than to a person, revocable and rotatable independently, high-entropy so
    no KDF is needed, never typed. Concretely, revoking a sysop's broker
    access today means changing the password they log into the BBS with, and
    since authenticate_psk() compares against cfg->sys_pass it revokes broker access for every other sysop at the same time. The residue is stated too: TLS-PSK is symmetric, so API keys fix reuse and revocation completely and
    the at-rest problem only partly, where client certificates need no shared secret at all.

    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 16:06:55 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/19db924a02bb8a593fa18f31
    Modified Files:
    docs/superpowers/specs/2026-07-31-hashed-password-storage-design.md
    Log Message:
    docs: reopen hashing-vs-encryption, and state what storage cannot touch

    The document read as though hashing were decided. It is not: the question
    was raised as encrypted password storage and encryption is still argued
    for. "Why encryption was rejected" is now "Hashing or encryption", the recommendation is labelled as a recommendation, and the choice is the
    first entry under open questions. Everything downstream -- pass.tab, the
    record format, the migration -- is compatible with either, since both
    store an opaque per-user credential that only the authentication path interprets.

    One thing about it is settled, as a requirement rather than a preference:
    no scheme may require an operator to type a key at startup. A BBS
    restarts when its machine does, unattended, and a design that will not
    come back up on its own is not a candidate however good its cryptography.

    A new section states what no storage design changes. Most protocols
    Synchronet speaks transmit the password and ask the server to check it -- terminal login, FTP PASS, HTTP Basic, SMTP/POP3/IMAP PLAIN and LOGIN --
    so the cleartext exists in the process on every login, in a stack buffer
    in the terminal case (login.cpp:31). A core dump from a server handling a
    login can therefore contain a live password, and it can reach swap.
    Zeroizing after use is hygiene, not a fix; the window can be shortened,
    not closed. This is the same fact as "a malicious sysop cannot be
    mitigated", stated from the other end, and only SCRAM or FIDO2 escape it.

    The key-storage question gets real detail instead of a gesture, starting
    with a correction of scale: encryption puts one master key in an OS
    facility, not thousands of user credentials -- the records stay in
    pass.tab as file data. A table now covers DPAPI, Credential Manager, CNG/DPAPI-NG, systemd-creds, direct TPM2 sealing, the kernel keyring and
    the macOS Keychain, with what each does unattended and where it fails. systemd-creds with TPM2 sealing is the best Linux fit; the kernel keyring
    is the wrong tool because it does not survive a reboot; the BSDs have
    nothing standard, which is the real portability cost. The Linux facts
    were measured on a Debian 13 host, and the Windows and macOS rows are
    flagged as documentation rather than measurement.

    The sentinel argument is rebuilt on the right foundation. It claimed a
    control byte was unmatchable because getstr() rejects anything below a
    space, which does not survive scrutiny: HTTP Basic and SMTP/IMAP PLAIN
    carry base64 that decodes to arbitrary bytes, makeuser takes a password
    from argv, and the JavaScript path takes what it is given. The guarantee
    comes instead from check_pass() (userdat.c:4302), which every
    password-setting path uses and which rejects anything shorter than MIN_PASS_LEN, so a sentinel of one to three bytes is something no account
    can legitimately hold.

    Unmatchability is not achievable at all against an adversary holding the
    file, since a comparison is against a value they can read and replay. So
    the sentinel's claim is narrowed to what is true: it converts "nobody is
    asked for a password, so everybody gets in" into "an attacker needs both
    the file and a reader predating the change". It should be randomly
    generated per installation rather than compiled in as a constant --
    neither survives reading the file, but a constant is defeated by reading
    the source.

    Finally, a copyedit pass. The document kept announcing that its points
    were worth stating, honest, or belonged in the document, which says
    nothing a reader cannot judge and pads every paragraph carrying an
    argument. Roughly twenty of those wrappers are gone; the claims they
    wrapped are unchanged.

    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 17:25:30 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/2728e599d58e9f86b59f3302
    Modified Files:
    docs/superpowers/specs/2026-07-31-hashed-password-storage-design.md
    Log Message:
    docs: correct what hashing actually costs, and what the sentinel can rest on

    Two claims in this design were wrong in the same way: each asserted that something could not happen, where the code says otherwise.

    The first was that encryption buys "a capability that will never be
    used". Synchronet recovers a stored password in at least four places
    today, and all four survive under encryption and die under hashing: the
    digest authentication mechanisms; mailing a user their forgotten password (login.js:90-134, on by default); auditing every account against
    password.can (badpasswords.js); and auditing every account for weak,
    obvious or shared passwords (chksetup.js:126-165).

    The last is not incidental. chksetup.js builds password_list[password]
    across every account precisely to find users who share one, so making
    that unanswerable is P1 -- the stated goal of this design. One of its own properties exists to destroy a capability a sysop has today and may
    reasonably value.

    So the argument is restated as what it is: not that reversibility is
    unused, but that each of those capabilities is one Synchronet is better
    off without. Digest authentication is the thing being argued against
    rather than an independent benefit. Mailing a password sends a live
    credential in cleartext to a mailbox the BBS does not control and leaves
    it there, when a reset does the same job. Retroactive auditing is
    replaceable by enforcement at set time, which check_pass() already
    performs on the interactive paths. Detecting shared passwords has no replacement and goes deliberately. That is a value judgment about which features should exist, and a sysop who disagrees about any of the four
    has a real argument for encryption. Only the failure mode -- total and retroactive on key disclosure, against hashing's per-account degradation
    -- survives independent of it.

    Two consequences elsewhere. login.js was missing from the cleartext-sink
    audit and is added, flagged as enabled by default. And the feature
    removals are re-attributed throughout: they are the price of choosing
    hashing, not of moving credentials out of user.tab, since encryption
    would keep every one of them. That is a materially different trade to
    put in front of a sysop.

    The second wrong claim was that check_pass() guarantees no account holds
    a value shorter than MIN_PASS_LEN, which would have made a short sentinel
    safe by construction. It does not. check_pass() is policy on three paths
    -- signup, the change-password flow, and an advisory entry point for
    scripts -- and the sysop-facing tools are where it is absent. The
    terminal user editor filters through getstr() but accepts any length
    including one byte; the standalone UserEdit GUI is a plain text field and enforces neither; user.security.password, makeuser and makeuser.js copy
    through unvalidated; and third-party tools, imports and text editors
    answer to nothing. An earlier justification from getstr() was wrong for
    the same reason and is also withdrawn.

    The design no longer rests on either. A collision is tolerable rather
    than impossible, since new code never reads USER_PASS and a colliding
    record affects one account on a reader predating the change, and
    migration verifies instead of assuming: the conversion pass scans every
    record for the chosen sentinel before writing any and picks another if it collides. The value is chosen to be implausible on every path rather than impossible on some -- non-printable, from a control range clear of tab,
    LF, CR and the attribute-code 0x01 -- and randomized per installation
    rather than compiled in, because a published constant would let anyone
    present the known byte to a downgraded reader and authenticate as anyone without the file at all. Resisting that needs entropy, so the sentinel is several bytes rather than one, which still returns most of LEN_PASS to
    the rest of the record.

    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 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