Everything on, phase 3: sign-in and recovery tell nothing, recovered passwords end sessions, deleted roots are gone everywhere
Owner decision 2026-10-04: fix the account privacy findings.
- Sign-in. Every failure answers "That username and password do not match." after the same work: an unknown login
is hashed against a decoy, and the comparison is constant-time. "Banned" is told only to someone who gave the right
password. This covers /clientapi/user/login, /invitation/login and /oauth/login.
- Recovery.
- Every request answers the same sentence and queues a SendRecovery job, whether or not the account exists or has an
email. The lookup, the code and SMTP move to RecoveryJob, so neither the answer nor its timing says anything.
- Codes are kept only as a SHA-256 hash, for one hour. Migration _011 drops the plaintext ones, which never expired.
- A recovered password ends every session of the root. RootSessions sets CredentialsChangedAt, which JwtEvents
checks against the JWT's issue time, now stamped as nbf, and revokes each persona's OAuth tokens and authorizations.
- Deleting a root (RootRemoval: the admin route, or the restored self-delete at /clientapi/user/delete, which asks for
the password).
- Its sessions end.
- Each persona and each group it owns sends Delete{Actor} to its followers, its members and the accounts it follows.
- The personas' posts are emptied.
- /peasants/{name} answers 410 with a Tombstone (formerType Person or Group), as do its inbox and WebFinger, through
LocalActorService.Gone. The names stay reserved.
- The root keeps only a unique `deleted-{id}` name; the second deletion on an instance used to collide on
"Deleted user".
Also, from phase 2's pasture: GoToSocial files a circle post like a DM and shows it only to accounts it mentions. Each
member's copy, and a member's refetch, now also mentions that member silently. The GoToSocial scenario checks circle
posts in conversations, like DMs, and they pass there now, as on Mastodon.
657 tests pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELjqpznMFMNrJoJUj6K5p2
This commit is contained in:
1 parent
fcd35f5043
commit
8c2eba6cbb
25 files changed
+665
-248
No files matched your search
@@ -252,6 +252,16 @@ group www-data and reaches the private mongod; `sudo -u www-data` works too.
|
||||
|
||||
- **No root id in federation output, NodeInfo or logs, no IP next to an identity in logs, and no `ex.Message` to a
|
||||
client** ("Something went wrong." instead).
|
||||
- **Sign-in and recovery never tell which logins or addresses exist** (owner decision 2026-10-04): every failed sign-in
|
||||
gets "That username and password do not match." after the same hashing (`RootUsersService.Decoy`, a constant-time
|
||||
comparison), and only a correct password learns of a ban. Every recovery request answers the same sentence and
|
||||
queues a `SendRecovery` job, found or not; `RecoveryJob` sends the email and keeps only a SHA-256 of the code, for an
|
||||
hour. A recovered password ends every session of the root (`RootSessions`: `CredentialsChangedAt` for `/clientapi`
|
||||
JWTs, OpenIddict revocation for each persona).
|
||||
- **Deleting a root deletes everything public it had** (`RootRemoval`, from the admin route or `/clientapi/user/delete`
|
||||
with the password): its sessions end, each persona and each group it owns sends `Delete{Actor}` to followers, members
|
||||
and the accounts it follows, the personas' posts are emptied, and `/peasants/{name}`, its inbox and WebFinger answer
|
||||
410 (`LocalActorService.Gone`) while the names stay reserved. The root keeps only a `deleted-{id}` name.
|
||||
- **Never derive a public name from the root username.** Invitation sign-up takes `AvatarUserName` and refuses one
|
||||
equal to the login.
|
||||
- **One username space:** personas, groups and the instance reserve their name in `ReservedName` (unique index) before
|
||||
|
||||
Reference in new issue
Block a user