Repository navigation
fix: implement chunked Windows secret storage functionality - #108
saschajohn wants to merge 1 commit into
Conversation
Add support for Windows secret storage with chunking and manifest validation.
| if let Some(existing_chunks) = &existing_chunks { | ||
| delete_windows_chunks(service, account, 0..existing_chunks.len())?; |
| let chunk_count = chunk_count | ||
| .parse::<usize>() | ||
| .context("parse Windows secret chunk manifest")?; | ||
| if chunk_count < 2 { | ||
| bail!("Windows secret chunk manifest must contain at least two chunks"); | ||
| } | ||
| Ok(Some(chunk_count)) |
| #[test] | ||
| fn windows_secret_helpers_preserve_utf8_boundaries() { | ||
| let secret = format!("{}🙂tail", "a".repeat(WINDOWS_SECRET_CHUNK_SIZE - 1)); | ||
| let chunks = utf8_chunks(&secret, WINDOWS_SECRET_CHUNK_SIZE).collect::<Vec<_>>(); |
peterj
left a comment
There was a problem hiding this comment.
thanks for the PR! can you address the comments please?
|
Tested this on a Windows 11 Enterprise VM (build 26300, not domain-joined) with the daemon installed as the
Building on the chunking here, I tried an approach for the three review points, as one commit on top of 58bc554: LutzLange@3804cd8
On the VM the tests pass as SYSTEM, including the Credential Manager round trip, and the service enrolls on the first attempt and reconnects after a restart without a new sign-in. Happy to adjust, or to open it against your branch if that's easier. |
|
is chunking really the best approach? or store a key (small) in the storage and encrypt the data (arbitrary size) on the disk? |
|
@howardjohn Agreed, chunking is much more complicated. I implemented your suggestion in #171: each secret is an AES-256-GCM file in the private state directory, and Credential Manager keeps only its 32-byte key. The Windows service enrolls as LocalSystem against Keycloak ( @saschajohn this builds on your analysis in #107. |
…ntial Manager Windows Credential Manager limits a credential to 2560 bytes, and OAuth credentials with refresh tokens exceed it, so enrolling the Windows service failed (agentdesktop-dev#107). Store each secret as an AES-256-GCM file in the daemon's private state directory and keep only its 32-byte random key in Credential Manager, as suggested in agentdesktop-dev#108. - The key is created once per secret and reused, so replacing the file atomically is the only step that changes a stored secret. - The file is bound to its service and account (AAD), so a file moved to another entry's path does not open. - Values stored directly in Credential Manager by earlier versions are still read, and are replaced on the next write. Delete removes such a value first, so a delete that stops halfway never brings an old value back. - The envelope logic sits behind a small key-store trait and is tested on every platform with an in-memory store and injected failures; one Windows test round-trips a 10,000-character secret through Credential Manager. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ntial Manager (#171) * fix(agent): store Windows secrets as encrypted files keyed from Credential Manager Windows Credential Manager limits a credential to 2560 bytes, and OAuth credentials with refresh tokens exceed it, so enrolling the Windows service failed (#107). Store each secret as an AES-256-GCM file in the daemon's private state directory and keep only its 32-byte random key in Credential Manager, as suggested in #108. - The key is created once per secret and reused, so replacing the file atomically is the only step that changes a stored secret. - The file is bound to its service and account (AAD), so a file moved to another entry's path does not open. - Values stored directly in Credential Manager by earlier versions are still read, and are replaced on the next write. Delete removes such a value first, so a delete that stops halfway never brings an old value back. - The envelope logic sits behind a small key-store trait and is tested on every platform with an in-memory store and injected failures; one Windows test round-trips a 10,000-character secret through Credential Manager. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: address review on Windows secret envelope - A secret file whose key is missing reads as absent (with a warning), so the GitHub, Anthropic and gateway OIDC flows sign in again instead of failing on every request. A malformed key or tampered file stays an error. - Only the first write removes an earlier version's Credential Manager value, so later writes skip that call. - The test credential store checks the limit in UTF-16 bytes, as keyring stores it. - aws-lc-rs is a Windows and dev dependency only. - One SecretStore::new branch for Linux and Windows; the key lookup returns an enum instead of a nested Option. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: address Copilot review on Windows secret envelope - A read that misses the file and then finds no earlier-version value looks at the file again: a concurrent first write stores the file before it removes that value, so the read no longer reports a secret that existed throughout as absent. Regression test for that interleaving. - The entry-binding test gives both entries the same key, so only the AAD binding can reject the copied file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: serialize Windows secret writes across processes The in-process mutex did not cover a second daemon instance, which runs secret-using tasks before it finds the first one on the named pipe. Two competing first writes could then leave one write's file next to the other's key. Writes and deletes now hold an exclusive lock on `secrets/.lock` (std File::lock, LockFileEx on Windows), which serializes threads and processes alike. Regression test with competing first writes, each on its own lock handle; the failed-file-write test now fails at the file replace rather than at a missing directory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: scope Windows file keys to their store and never pair a new key with an old file - Credential Manager is shared by everything running as one Windows account, while the files and the lock are per state directory. Each secrets directory now has a random identity (`secrets/.store-id`), and file keys live at `<service>.file-key` / `<store id>/<account>`, so two state directories no longer overwrite or delete each other's keys. - When a write has to create a new key (missing or malformed), it removes the old file first: nothing can open it, and a later failure then leaves a secret that reads as absent instead of a file next to the wrong key. - Regression tests for both, with two directories on one key store and a check that no file exists when a new key is stored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: treat a non-UTF-8 store identity as malformed `read_to_string` failed with InvalidData on such a file, which blocked every write until the file was removed by hand. Read the bytes and treat anything that is not a valid identity as malformed, so the next write replaces it. Other filesystem errors still propagate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: take the store lock shared for reads A read could load a file, then a delete and a new write could replace its key before the read looked the key up; the read then reported the old file as corrupt, which identity::load turns into IdentityUnreadable. Reads now hold the store lock shared (writes and deletes hold it exclusively), so reads still run in parallel but never span a replace. This also covers the first-write race, so the re-read of the file in get() is gone. Test: a writer on its own handle cannot take the lock while a read looks up the key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: fail the file write of the secret being updated The failed-write test failed a write to another path, so it would pass even if a failed update destroyed its own file. A test-only switch now fails the next file write; the test updates the stored secret itself and checks that the file and value are unchanged (it fails if set() removes the file before writing). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>



Add support for Windows secret storage with chunking and manifest validation.
Fixes #107