You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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)
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.
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.
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.
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
2025.9.0(nixpkgs package, unpatched upstream code)cryptpad/ssoat4f77fca4a9e937fbbc7c189da41fc126133c641aenforced: true,cpPassword: true,forceCpPassword: truehttpSafeOriginset 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):
jlf7ZF+X…NURx27ND…—BLOCK_WRITE_BY_OWNER,isChange: truepABQSODm…→ 404Server log for the change itself:
Access log shortly after:
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)
Fallback to the account name.
settings/inner.js:694usesssoSeed || privateData.accountName. If the seed had been missing, the derivedoldByteswould have been wrong andQ_SETTINGS_CHECK_PASSWORDwould have aborted before writing anything. A block was written, so the old password verified correctly — the name used for derivation was right.Lowercasing mismatch.
ssoauth/main.js:65stores the seed asseed.toLowerCase(), andcommon-login.js:294lowercases 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 inlocalStorage['SSO_seed']contains only digits and lowercase — i.e. exactly the lowercased form, as intended. Both paths agree.Different derivation.
deriveBytes(settings/inner.js:445-447) callsCred.deriveFromPassphrase(name, password, Login.requiredBytes, cb)— the same function with the same parameters ascommon-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: trueis 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:615already does this whensso.password === 0) would at least prevent the loss until it is understood.Happy to run further tests against this instance if that helps.