Skip to content

Split Windows token storage across Credential Manager entries - #68

Open
aberoham wants to merge 1 commit into
osodevops:mainfrom
aberoham:fix/windows-credential-chunking
Open

Split Windows token storage across Credential Manager entries#68
aberoham wants to merge 1 commit into
osodevops:mainfrom
aberoham:fix/windows-credential-chunking

Conversation

@aberoham

Copy link
Copy Markdown
Contributor

Resolves #67.

On Windows, teams auth login completed the sign-in and then failed to persist the token with KEYRING_ERROR: ... longer than platform limit of 2560 chars. Credential Manager caps a credential blob at 2560 bytes (CRED_MAX_CREDENTIAL_BLOB_SIZE), and the keyring crate enforces that limit before calling CredWriteW. A Microsoft Graph token bundle is routinely larger than that, and as the issue notes the size is set by the tenant's consent grant rather than by the scopes the CLI requests, so it cannot be shrunk from the client side.

What changes

On Windows the serialized token is now split across several Credential Manager entries:

  • <profile>:token holds a small JSON header, {"chunks": N}.
  • <profile>:token:0 through <profile>:token:N-1 hold the token bytes, written with Entry::set_secret so each chunk may use the full 2560 bytes (a UTF-16 password would halve that).

Chunks are written before the header, so a reader never follows a header to chunks that do not exist yet. After each write, chunks left over from a previous larger token are deleted: every index the old header recorded is attempted whether or not it exists, then the sweep continues until the first missing index, so a gap left by an earlier partial failure cannot hide later chunks. teams auth logout does the same sweep and deletes the header last, so a retried logout still knows how many chunks to expect. A header pointing at a missing chunk reports "Stored token is incomplete ... Run teams auth login again" rather than a raw keyring error.

Credential Manager has no transactions, so a write interrupted part-way, or two processes refreshing the same profile at once, can leave the header describing a mix of old and new chunks. That state either fails to parse or yields a spliced token that Graph rejects; in both cases the next refresh or login rewrites every entry consistently, so the scheme is self-healing rather than atomic. The code comment says as much. I did not add generation-numbered chunk sets because they would only cover the interruption case, not the concurrent one (both writers would pick the same next generation), and the recovery is the same either way.

One behaviour change that is not Windows-specific: auth logout now reports a failure to delete the stored token instead of swallowing it and claiming success (the old code called .ok() on the delete). A profile with no stored token still logs out cleanly.

Token storage on macOS and Linux is otherwise unchanged: one keychain item per profile, no additional keychain reads, writes or deletes (which matters on macOS, where each item carries its own access grant), and a plain-JSON item written by an earlier version still reads.

The token logic now runs against a small SecretStore trait with an OS keyring implementation, so the chunking path is unit-tested with an in-memory store on every platform (round trip, exact-multiple boundary, stale-chunk cleanup on shrink with and without a gap, legacy read, missing chunk, logout sweep with and without a header or a gap, multi-byte UTF-8 split at every small chunk size). The keyring crate's own mock store does not persist across Entry instances, which is why the tests do not use it.

Why chunking rather than the encrypted-file fallback

The product requirements document mentions an encrypted-file fallback. Chunking keeps tokens in the OS credential store, which is what SECURITY.md promises, and avoids adding a key-derivation scheme or a DPAPI dependency. The file fallback remains future work if a platform without a usable keyring needs it.

Testing

  • cargo fmt --check, cargo clippy --all-targets -- -D warnings, cargo test --all-targets pass on macOS.
  • CI runs the unit tests on windows-latest, so the chunking path is exercised there. I have not been able to run the binary against a real Windows Credential Manager; a build from this branch on the reporter's machine would be the definitive check.

Windows Credential Manager caps a credential blob at 2560 bytes
(CRED_MAX_CREDENTIAL_BLOB_SIZE). A Microsoft Graph token bundle is
routinely larger, so on Windows `teams auth login` completed the sign-in
and then failed to persist the token with `KEYRING_ERROR ... longer than
platform limit of 2560 chars`, leaving the CLI unauthenticated.

On Windows the serialized token is now written as raw-byte chunks in
`<profile>:token:<n>` entries, with the `<profile>:token` entry holding a
`{"chunks": N}` header. Chunks are written before the header so a reader
never follows a header to missing chunks. After each write, and on
`auth logout`, every chunk index the previous header recorded is deleted
whether or not it exists and the sweep then continues to the first missing
index, so a gap left by an earlier partial failure cannot orphan later
chunks; logout deletes the header last so a retry still knows the count.
A header that points at a missing chunk reports the token as incomplete
and asks for a new login. Credential Manager has no transactions, so an
interrupted or concurrent write can leave a spliced token; it fails to
parse or is rejected by Graph, and the next refresh or login rewrites
every entry.

Token storage on macOS and Linux is unchanged: one keychain item per
profile, no extra keychain operations, and a plain-JSON item written by an
earlier version still reads. `auth logout` now propagates a failure to
delete the stored token on every platform instead of reporting success.
The token logic runs against a small `SecretStore` trait so it is
unit-tested with an in-memory store everywhere.

Resolves osodevops#67.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HZKxuyb6aMaLpusWpLs2so
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Windows: login succeeds but token storage fails — bundle exceeds Credential Manager's 2560-char blob limit

1 participant