Skip to content

Read Cursor v10 tokens without writing them back - #83

Open
JoaoFellows wants to merge 7 commits into
poweruserz:mainfrom
JoaoFellows:fix/cursor-safe-storage
Open

JoaoFellows wants to merge 7 commits into
poweruserz:mainfrom
JoaoFellows:fix/cursor-safe-storage

Conversation

@JoaoFellows

@JoaoFellows JoaoFellows commented Nov 11, 2026 •

Copy link
Copy Markdown

Description

Cursor now stores the desktop login as cursor-safe-storage:v1: values in state.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 cursor plugin 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-sys was already a dependency. This adds the Win32_Security_Cryptography and Win32_System_Memory features it needs for DPAPI. No new crate.

Related Issue

Fixes #82

Type of Change

  • Bug fix
  • New feature
  • New provider plugin
  • Documentation
  • Performance improvement
  • Other (describe below)

Testing

  • I ran bun run build and it succeeded
  • I ran bun run test and all tests pass
  • I tested the change locally with bun tauri dev

Also ran the checks from CONTRIBUTING.md: bunx tsc --noEmit, bun run test (1557 passed), and cargo test --lib (281 passed, 0 failed). The live tray app was not started.

Checklist

  • I read CONTRIBUTING.md
  • My PR targets the main branch
  • I did not introduce new dependencies without justification

JoaoFellows and others added 5 commits November 9, 2026 23:05
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>
@greptile-apps

greptile-apps Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; both previous findings are fixed and no new actionable issues remain.

Summary

Adds support for Cursor's encrypted Windows login without replacing encrypted database values with plaintext.

  • Cursor usage reads encrypted desktop sign-ins.
  • Refreshed Cursor tokens stay protected outside its database.

Both previous findings are addressed. No new actionable issues were found. The previous-review merge bases differed, so this review covered the full PR changes.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Read Cursor login] --> B{Encrypted value?}
  B -->|Yes| C[Decrypt with Windows key]
  B -->|No| D[Use stored token]
  C --> E{Token needs refresh?}
  D --> E
  E -->|No| U[Read usage]
  E -->|Yes| F{Matching saved token?}
  F -->|Yes| U
  F -->|No| R[Refresh token]
  R --> P{Encrypted source?}
  P -->|Yes| S[Save DPAPI-protected token]
  P -->|No| W[Save to original source]
  S --> U
  W --> U
Loading

Reviews (2) · Last reviewed commit: "Merge upstream main into fix/cursor-safe..." · Reviewed by Greptile

Comment thread plugins/cursor/plugin.js Resolved
Comment thread plugins/cursor/plugin.js Resolved

@poweruserz poweruserz left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. The refreshed token is cached in plain text (writeCachedAccessToken in plugins/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 through host.fs.readText. Could the cache go through the host as well (a protect call beside decrypt), and be dropped when no login is found?
  2. 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 into state.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 sub only (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_Memory isn't needed: LocalFree is in Win32::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?

@JoaoFellows

Copy link
Copy Markdown
Author

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.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cursor card cannot read v10 safe-storage auth tokens

2 participants