Repository navigation
Read Cursor v10 tokens without writing them back - #83
JoaoFellows wants to merge 7 commits into
Conversation
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
[Medium impact] The PR appears safe to merge; both previous findings are fixed and no new actionable issues remain.
|
poweruserz
left a comment
There was a problem hiding this comment.
Thanks for this, and for the tests and all 11 translations. The host side looks right to me: the call is gated on the exact plugin id, the path to Local State is fixed in Rust, lengths are checked before slicing, the tag is verified, and nothing logs key material. Before merging I'd like two things changed in the plugin:
- The refreshed token is cached in plain text (
writeCachedAccessTokeninplugins/cursor/plugin.js). Cursor now keeps this token DPAPI-protected, and the cache puts a bearer token unencrypted in%APPDATA%\com.poweruserz.opentokenusage\plugins_data\cursor\access-token.json. Signing out of Cursor doesn't clear it either, and as Greptile notes, any plugin can read that file throughhost.fs.readText. Could the cache go through the host as well (a protect call besidedecrypt), and be dropped when no login is found? - A plaintext write to Cursor's database is still possible (
loadAuthState).writeSqlite = !isSafeStorageValue(sqliteAccessRaw)looks only at the access row. When that row is missing or unreadable and the refresh row is encrypted, a refresh writes a plain JWT intostate.vscdb. Treating either row being encrypted as "don't write", with a test, would close it.
Smaller, your call:
- The cache is matched by the JWT's
subonly (and accepted when either token has none) and is preferred even over a fresh token in the database. After signing in again the older token is used, and a 401 on it neither drops it nor falls back to the database's token. Binding it to the stored refresh token (a hash, as the antigravity plugin does) would fix both. Win32_System_Memoryisn't needed:LocalFreeis inWin32::Foundation.- A gate test for an id that only starts with
cursor(cursor-x) would pin the exact match.
I couldn't reproduce #82 here: my Cursor still stores plain JWTs. Which Cursor version writes cursor-safe-storage:v1: values for you?
Co-authored-by: Cursor <cursoragent@cursor.com>
|
Cursor 3.29.15 (stable) on this machine. The failure started right after that update. This build's resources/app/out/main.js contains the cursor-safe-storage:v1: codec. Cursor 3.27 turned on cursor.storage.authTokenEncryptionAtRest and started storing cursorAuth/accessToken and cursorAuth/refreshToken with that prefix (reported on 3.27.12; also seen on 3.28.5). 3.29.15 is after that. Plain JWTs are still what you get on a build before 3.27, or when encryption at rest is off. The two changes you asked for are in this branch now. Either encrypted row blocks a SQLite write. The refreshed access token is DPAPI-protected through cursorAuth.protect and dropped when no login is found. Also done: the cache is kept only when the loaded token needs a refresh, both subjects match, and the refresh-token hash matches; a 401 drops it and can fall back to the database token. Win32_System_Memory is removed. cursor-x does not receive cursorAuth. The branch is updated onto 0.8.4. |
Description
Cursor now stores the desktop login as
cursor-safe-storage:v1:values instate.vscdb. OpenTokenUsage treated those values as plaintext JWTs, so the Cursor card could not load usage. Writing a refreshed access token back into that row would replace the encrypted blob with plaintext.The host decrypts the value with the Windows DPAPI key, and only the
cursorplugin receives that call. When the stored token is encrypted, the refreshed access token is kept in the plugin data directory for the next probe and is not written back to Cursor's database.windows-syswas already a dependency. This adds theWin32_Security_CryptographyandWin32_System_Memoryfeatures it needs for DPAPI. No new crate.Related Issue
Fixes #82
Type of Change
Testing
bun run buildand it succeededbun run testand all tests passbun tauri devAlso ran the checks from CONTRIBUTING.md:
bunx tsc --noEmit,bun run test(1557 passed), andcargo test --lib(281 passed, 0 failed). The live tray app was not started.Checklist
mainbranch