Skip to content

Password change on an SSO account writes a login block the login flow never looks for — account becomes unreachable #28

Description

@achimcc

Changing the CryptPad password of an SSO account writes a login block that the login flow never looks for. The old block is archived in the same operation, so the account becomes unreachable — with a success message and nothing in the logs to indicate a problem.

Recovered here only from a filesystem snapshot taken minutes before. On a zero-knowledge service the operator cannot restore an account, so this is silent data loss.

Versions

  • CryptPad 2025.9.0 (nixpkgs package, unpatched upstream code)
  • cryptpad/sso at 4f77fca4a9e937fbbc7c189da41fc126133c641a
  • IdP: Authentik, OIDC, enforced: true, cpPassword: true, forceCpPassword: true
  • Single account on the instance, logged in via SSO, httpSafeOrigin set to a separate domain (no trailing-slash problem as in No button event on setting CP password after login #9)

What happens

Three different block addresses are involved (truncated to 8 chars):

step block
account before the change jlf7ZF+X…
the change writes NURx27ND…BLOCK_WRITE_BY_OWNER, isChange: true
something then requests pABQSODm…404

Server log for the change itself:

["INFO",…,"BLOCK_WRITE_BY_OWNER",{"blockId":"NURx27ND…","isChange":true,"previousKey":"jlf7ZF+X…"}]
["INFO",…,"ARCHIVAL_BLOCK_BY_OWNER_RPC",{"publicKey":"jlf7ZF+X…","status":"SUCCESS"}]

Access log shortly after:

GET /block/pA/pABQSODm… 404

After this, the account cannot be logged into with either the old or the new password.

What I ruled out (measured, so you don't have to)

  1. Fallback to the account name. settings/inner.js:694 uses ssoSeed || privateData.accountName. If the seed had been missing, the derived oldBytes would have been wrong and Q_SETTINGS_CHECK_PASSWORD would have aborted before writing anything. A block was written, so the old password verified correctly — the name used for derivation was right.

  2. Lowercasing mismatch. ssoauth/main.js:65 stores the seed as seed.toLowerCase(), and common-login.js:294 lowercases the name on every login ("Usernames are all lowercase. No going back on this one"). Verified on the instance: the seed stored server-side contains uppercase characters, the value in localStorage['SSO_seed'] contains only digits and lowercase — i.e. exactly the lowercased form, as intended. Both paths agree.

  3. Different derivation. deriveBytes (settings/inner.js:445-447) calls Cred.deriveFromPassphrase(name, password, Login.requiredBytes, cb) — the same function with the same parameters as common-login.js:315.

So the derivation input appears correct at change time (the old-password check passes), yet the resulting block is not where the next login looks. I could not narrow it down further without instrumenting the client.

Why this matters more than a normal bug

forceCpPassword: true is what makes the setup zero-knowledge — the server holds the seed, the user's password is the part it does not know. Users who take that seriously are exactly the ones likely to rotate their password, and the operation destroys their account while reporting success.

If the cause is not obvious, hiding the "change password" section for SSO accounts (the branch at settings/inner.js:615 already does this when sso.password === 0) would at least prevent the loss until it is understood.

Happy to run further tests against this instance if that helps.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions