Repository navigation
fix(cli): sign in to auth-v2 accounts without ever sending the password - #8
Merged
Merged
Conversation
chogarcia
marked this pull request as ready for review
September 30, 2026 21:51
…ive is missing - Salt lookup network errors and 429 now abort with a clear message before anything is posted; the password is only sent for an account the server affirmatively reports as version 1, and only to the version-aware login route. - test/auth.noPasswordLeak.test.mjs captures every outbound request (signIn fetch, global fetch, every Supabase client call) across v2 success, v2 MFA step-up, v2 401/429/unreachable, ten salt-lookup failure shapes and both v1 arms, and asserts where the password may appear. - argon2 moves to optionalDependencies (still pinned 0.45.1). A missing native module falls back to @shieldfive/crypto's libsodium WASM path, so npm i -g never fails on a platform without a prebuilt binary. Tests pin native == WASM == @shieldfive/crypto for moderate (16/32-byte salts) and sensitive. - Vault unlock tries the trimmed password first, as the web keyring wraps with, then the password as typed. The login secret stays untrimmed, as web sign-in, signup, reset and change-password derive it. - Email is trimmed and lowercased before the salt lookup, as on Android. - Drop docs/desktop-readiness.md: desktop planning is not this fix and does not belong in the public repo. - CHANGELOG Unreleased entry; README layout.
chogarcia
force-pushed
the
fix/auth-v2-desktop
branch
from
September 30, 2026 21:52
f60c879 to
b4bbc72
Compare
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
0.3.0 posts the raw password straight to Supabase GoTrue (
src/auth.mjs). Accounts on the client-derived login secret (auth v2: every new account, and every account migrated on its next web/app sign-in) cannot sign in throughsf login/sf push/sf sync. Each failed attempt also sends the password to the server.What changes
POST /api/auth/saltfirst, thenPOST /api/mobile/auth/login, then the session is installed in supabase-js and the existing TOTP step-up runs.{ email, loginSecret }. The password is not sent.{ email, password, loginSecret, authSalt }. This is the web/Android migration body: the server checks the password, then rotates the account to v2.{ email, password }.fetchAuthSaltfalls back to v1 here. The CLI deliberately does not: for a v2 account that fallback would leak the password and still fail to sign in.base64url(Argon2id("shieldfive/v1/auth/login-secret\n" + password, authSalt, moderate)): Argon2id v0x13, t=3, m=256 MiB, p=1, 32 bytes. Salts longer than 16 bytes go through HKDF-SHA-256 compression (zero salt, infoshieldfive/v1/argon2id/salt-compression). The salt can be base64 or base64url. The password is used exactly as typed, not trimmed. That matches web sign-in, signup, reset and change-password (resetPasswordClientForm.tsxandsettingsContentClient.tsxboth say "NOT trimmed"), so it is what the secret stored in GoTrue was derived from.utils/keyring.ts) trims before every wrap and unwrap. If AES-GCM rejects it and the password has surrounding whitespace, it retries with the password as typed. Before this, a vault password with leading or trailing spaces could not open a web-created vault from the CLI.argon2@0.45.1, pinned exactly, is now an optionalDependency. When the native module is present it runs async on the libuv pool, so the event loop keeps running. When it is missing,src/argon2.mjsfalls back to@shieldfive/crypto's libsodium WASM path, which gives identical bytes and is always installed. Parameters are unchanged on both paths.docs/desktop-readiness.mdis removed from the PR. It is desktop planning, not part of this fix, and it named a customer in a public repo. The text is still in commit 4910371 if you want to move it somewhere private.Verified
YRxqCqxx…(16-byte salt) and-3MAPAD6…(32-byte salt). These are the same vectors Android'sloginSecret.test.tspins, produced by the real web library. They pass on the native backend and, in a fresh process, on the forced WASM backend.@shieldfive/cryptoderiveMasterSecret (the web vault reference) for moderate with 16- and 32-byte salts and a Unicode passphrase, and for sensitive (1 GiB). That closes the "sensitive-preset interoperability pending" gap.web/app/api/auth/salt/route.ts,app/api/mobile/auth/login/route.tsandutils/auth/loginSecretSignIn.ts: request shapes, v2 refusing a body withoutloginSecret, and theaccessToken/refreshTokenresponse fields.nextLevel=aal2challenges and verifies, returns the AAL2 token, and returns the refresh token from the stepped-up session. The existingauth.mfatests are unchanged and pass.npm packand thennpm i -gfrom the tarball into a temp prefix twice. On macOS arm64 the native backend is used. With the native build forced to fail (npm_config_build_from_source=true CC=false), the install still exits 0, npm dropsargon2,sfruns, and the backend iswasm. argon2 0.45.1 ships prebuilds for darwin-arm64, linux-x64/arm64/armv7 (glibc and musl), win32-x64 and freebsd. It has none for Intel macOS or Windows on ARM; those get the WASM path (slower, same keys). Its only install script isnode-gyp-build, which uses a prebuild or compiles. There is no network fetch and no other postinstall.No-password-leak test
test/auth.noPasswordLeak.test.mjsrecords everything that leaves the process: every call tosignIn's fetch, the globalfetch(which is stubbed to trap), and every argument passed to the Supabase client (createClient,setSession,mfa.challenge,mfa.verify,signInWithPassword). It then searches each URL and body for the password:POST /api/mobile/auth/login. It never goes to the salt lookup or Supabase.supabase.auth.signInWithPasswordand the globalfetchare never called.Tests
npm test: 127/127 pass locally (163 s; the 1 GiB sensitive interop test takes most of that).Release (founder)
Follows the repo convention from #7 ("release: 0.3.0"): the fix PR adds an
## UnreleasedCHANGELOG entry, and the version bump goes in a separate release PR. There is no version bump here. This repo has no publish workflow, so a tag push does not publish; publishing is manual.release: 0.3.1: bumppackage.jsonto 0.3.1, runnpm install --package-lock-onlyso the lockfile matches, and rename## Unreleasedto## 0.3.1 - <date>(keep an empty## Unreleasedabove it). Merge.npm ci && node src/cli.mjs loginagainst a disposable QA account that is already on auth v2. Thensf pushone small file,sf verifyit, andsf logout.npm publish(withnpm loginfirst if needed), thengit tag v0.3.1 && git push origin v0.3.1so the tag marks what was published.npm i -g @shieldfive/cli@0.3.1 && sf --help.