Split Windows token storage across Credential Manager entries - #68
Open
aberoham wants to merge 1 commit into
Open
Split Windows token storage across Credential Manager entries#68aberoham wants to merge 1 commit into
aberoham wants to merge 1 commit into
Conversation
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
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.
Resolves #67.
On Windows,
teams auth logincompleted the sign-in and then failed to persist the token withKEYRING_ERROR: ... longer than platform limit of 2560 chars. Credential Manager caps a credential blob at 2560 bytes (CRED_MAX_CREDENTIAL_BLOB_SIZE), and thekeyringcrate enforces that limit before callingCredWriteW. 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>:tokenholds a small JSON header,{"chunks": N}.<profile>:token:0through<profile>:token:N-1hold the token bytes, written withEntry::set_secretso 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 logoutdoes 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 ... Runteams auth loginagain" 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 logoutnow 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
SecretStoretrait 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). Thekeyringcrate's own mock store does not persist acrossEntryinstances, 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.mdpromises, 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-targetspass on macOS.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.