-
Notifications
You must be signed in to change notification settings - Fork 1.4k
fix(credential-store): mirror .encryption_key instead of deleting it (fixes silent auth wipe)#845
Conversation
🦋 Changeset detectedLatest commit: b9c028a The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Summary of ChangesHello, I'm Gemini Code Assist1 ! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request resolves a critical issue in the credential management logic where the application would incorrectly delete the Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).
View this failed invocation of the CLA check for more information.
For the most up to date status, view the checks section at the bottom of the pull request.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request modifies the credential store to mirror the keyring's key to the .encryption_key file as a durable fallback for headless/CI environments, rather than deleting it. The review feedback highlights a security concern where using std::fs::write directly does not guarantee restrictive file permissions or atomicity. It is recommended to use the existing ensure_key_dir and save_key_file helpers to write the key securely.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using std::fs::write directly to write the encryption key is insecure because it does not guarantee restrictive file permissions (such as 0o600) and is not atomic, which could lead to key file corruption if interrupted.
Instead, use the existing ensure_key_dir helper to secure the parent directory and save_key_file to write the key atomically with secure permissions.
let _ = ensure_key_dir(key_file); if let Err(e) = save_key_file(key_file, b64_key.trim()) { eprintln!( "Warning: failed to mirror keyring key to '{}': {}", key_file.display(), e ); }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using std::fs::write directly to write the encryption key is insecure because it does not guarantee restrictive file permissions (such as 0o600) and is not atomic, which could lead to key file corruption if interrupted.
Instead, use the existing ensure_key_dir helper to secure the parent directory and save_key_file to write the key atomically with secure permissions.
let _ = ensure_key_dir(key_file); if let Err(e) = save_key_file(key_file, b64_key.trim()) { eprintln!( "Warning: failed to mirror keyring key to '{}': {}", key_file.display(), e ); }
b887cf0 to
be21424
Compare
googleworkspace-bot
commented
Jun 12, 2026
/gemini review
be21424 to
12c9e32
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request updates the credential store to mirror the keyring's key to the .encryption_key file as a durable fallback for headless/CI environments, rather than deleting it. The review feedback points out that using std::fs::write is non-atomic and could lead to file corruption. It recommends utilizing existing helper functions (ensure_key_dir and save_key_file) to handle secure directory creation and atomic writes, as well as sanitizing error messages to prevent terminal escape sequence injection.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current implementation uses std::fs::write, which is not atomic. This could lead to a corrupted .encryption_key file if the process is interrupted, which would be as severe as the bug this PR aims to fix. Additionally, std::fs::create_dir_all is used without setting secure permissions on the directory, and the error message is not sanitized, which violates a repository rule.
To address these issues, you should use the existing helper functions:
ensure_key_dirto create the directory with secure permissions (0700).save_key_filewhich performs an atomic write.sanitize_for_terminalon the error message.
if let Err(e) = ensure_key_dir(key_file) { eprintln!( "Warning: failed to create secure directory for '{}': {}", key_file.display(), sanitize_for_terminal(&e.to_string()) ); } if let Err(e) = save_key_file(key_file, b64_key.trim()) { eprintln!( "Warning: failed to mirror keyring key to '{}': {}", key_file.display(), sanitize_for_terminal(&e.to_string()) ); }
References
- Sanitize error strings printed to the terminal to prevent escape sequence injection.
googleworkspace-bot
commented
Jun 12, 2026
/gemini review
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request fixes a bug where the keyring backend was deleting the .encryption_key fallback file when a key was found in the OS keyring. Instead of deleting, the key is now mirrored to the fallback file to support headless and CI environments. The review feedback highlights that performing an atomic write with fsync on every CLI invocation is inefficient and introduces unnecessary startup latency and disk wear. It is recommended to check if the file already exists and contains the correct key before writing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Performing an atomic write (which includes an fsync call) on every single CLI invocation is highly inefficient and introduces unnecessary startup latency and disk wear. Since the key in the keyring is already mirrored to the .encryption_key file in most cases, we should first check if the file already exists and contains the correct key before attempting to write to it.
let mut needs_write = true; if let Some(existing_key) = read_key_file(key_file) { if existing_key == arr { needs_write = false; } } if needs_write { if let Err(e) = ensure_key_dir(key_file) { eprintln!( "Warning: failed to set up key directory '{}': {}", key_file.display(), e ); } if let Err(e) = save_key_file(key_file, b64_key.trim()) { eprintln!( "Warning: failed to mirror keyring key to '{}': {}", key_file.display(), e ); } }
2cf2292 to
b64509e
Compare
googleworkspace-bot
commented
Jun 12, 2026
/gemini review
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request fixes a silent authentication wipe issue where the keyring backend in resolve_key() was deleting the .encryption_key fallback file when a key was found in the OS keyring. Instead of deleting the file, the implementation now mirrors the keyring key to the fallback file using save_key_file and ensure_key_dir to ensure restrictive permissions and race-free writes. The corresponding unit tests have been updated to verify this mirroring behavior. I have no additional feedback to provide.
b64509e to
65655d8
Compare
googleworkspace-bot
commented
Jun 12, 2026
/gemini review
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request fixes a silent auth wipe issue by mirroring the keyring key to the .encryption_key fallback file instead of deleting it, and optimizes writes by checking if the file already matches the key. The reviewer pointed out that if directory creation fails, the subsequent file write will also fail, leading to redundant I/O and duplicate warning messages; they suggested only attempting to write the key file if the directory setup succeeds.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If ensure_key_dir fails, the parent directory cannot be created, which guarantees that save_key_file will also fail. This results in redundant I/O attempts and duplicate warning messages printed to the terminal. We should only attempt to write the key file if the directory setup succeeds.
match ensure_key_dir(key_file) { Ok(()) => { if let Err(e) = save_key_file(key_file, b64_key.trim()) { eprintln!( "Warning: failed to mirror keyring key to '{}': {}", key_file.display(), sanitize_for_terminal(&e.to_string()) ); } } Err(e) => { eprintln!( "Warning: failed to set up key directory '{}': {}", key_file.display(), sanitize_for_terminal(&e.to_string()) ); } }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If ensure_key_dir fails, the parent directory cannot be created, which guarantees that save_key_file will also fail. This results in redundant I/O attempts and duplicate warning messages printed to the terminal. We should only attempt to write the key file if the directory setup succeeds.
match ensure_key_dir(key_file) { Ok(()) => { if let Err(e) = save_key_file(key_file, b64_key.trim()) { eprintln!( "Warning: failed to mirror keyring key to '{}': {}", key_file.display(), sanitize_for_terminal(&e.to_string()) ); } } Err(e) => { eprintln!( "Warning: failed to set up key directory '{}': {}", key_file.display(), sanitize_for_terminal(&e.to_string()) ); } }
65655d8 to
e53c061
Compare
googleworkspace-bot
commented
Jun 12, 2026
/gemini review
e53c061 to
fcc2e91
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request fixes a silent authentication wipe issue by mirroring the keyring key to the .encryption_key fallback file instead of deleting it when a key is found in the OS keyring. The key is only written if it differs from the existing file content to avoid unnecessary disk writes. The review feedback suggests optimizing the error handling during this mirroring process: if setting up the key directory fails, the subsequent attempt to save the key file should be skipped using an else if block to prevent redundant I/O and duplicate warning messages.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If ensure_key_dir fails, the parent directory cannot be created or accessed, meaning any subsequent call to save_key_file is guaranteed to fail. This results in redundant I/O operations and duplicate warning messages printed to the terminal. Using else if prevents the write attempt when directory setup fails.
if let Err(e) = ensure_key_dir(key_file) { eprintln!( "Warning: failed to set up key directory '{}': {}", key_file.display(), sanitize_for_terminal(&e.to_string()) ); } else if let Err(e) = save_key_file(key_file, b64_key.trim()) { eprintln!( "Warning: failed to mirror keyring key to '{}': {}", key_file.display(), sanitize_for_terminal(&e.to_string()) ); }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If ensure_key_dir fails, the parent directory cannot be created or accessed, meaning any subsequent call to save_key_file is guaranteed to fail. This results in redundant I/O operations and duplicate warning messages printed to the terminal. Using else if prevents the write attempt when directory setup fails.
if let Err(e) = ensure_key_dir(key_file) { eprintln!( "Warning: failed to set up key directory '{}': {}", key_file.display(), sanitize_for_terminal(&e.to_string()) ); } else if let Err(e) = save_key_file(key_file, b64_key.trim()) { eprintln!( "Warning: failed to mirror keyring key to '{}': {}", key_file.display(), sanitize_for_terminal(&e.to_string()) ); }
googleworkspace-bot
commented
Jun 12, 2026
/gemini review
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request resolves a silent authentication wipe issue by mirroring the keyring's key to the .encryption_key fallback file instead of deleting it when a key is found in the OS keyring. The write is optimized to only occur when the file content differs from the keyring key, and relevant tests have been updated and added. The review feedback suggests a minor improvement to skip calling save_key_file if ensure_key_dir fails, preventing redundant system calls and duplicate warning logs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If ensure_key_dir fails, the parent directory does not exist or has incorrect permissions, meaning any subsequent attempt to write the key file via save_key_file is guaranteed to fail. We should avoid calling save_key_file if ensure_key_dir returns an error to prevent redundant system calls and duplicate warning logs.
if let Err(e) = ensure_key_dir(key_file) { eprintln!( "Warning: failed to set up key directory '{}': {}", key_file.display(), sanitize_for_terminal(&e.to_string()) ); } else if let Err(e) = save_key_file(key_file, b64_key.trim()) { eprintln!( "Warning: failed to mirror keyring key to '{}': {}", key_file.display(), sanitize_for_terminal(&e.to_string()) ); }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If ensure_key_dir fails, the parent directory does not exist or has incorrect permissions, meaning any subsequent attempt to write the key file via save_key_file is guaranteed to fail. We should avoid calling save_key_file if ensure_key_dir returns an error to prevent redundant system calls and duplicate warning logs.
if let Err(e) = ensure_key_dir(key_file) { eprintln!( "Warning: failed to set up key directory '{}': {}", key_file.display(), sanitize_for_terminal(&e.to_string()) ); } else if let Err(e) = save_key_file(key_file, b64_key.trim()) { eprintln!( "Warning: failed to mirror keyring key to '{}': {}", key_file.display(), sanitize_for_terminal(&e.to_string()) ); }
The keyring backend in resolve_key() was auto-deleting the .encryption_key
fallback file when a key was found in the OS keyring. The function
docstring explicitly stated the file should never be deleted ('it always
serves as a durable fallback for environments where the keyring is
ephemeral'), but the code contradicted the doc.
This caused a TOCTOU bug: when a user switched backends (e.g. from
GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND=file to default keyring), the keyring
would return a stale entry from before the file backend was used, then
delete the .encryption_key file (which contained the actual current key).
This made credentials.enc undecryptable, triggering a second cleanup
that deleted the credentials themselves — silently wiping the user's
authenticated session.
The fix mirrors the keyring key to the .encryption_key file (keeping the
file in sync with the keyring) instead of deleting it. Per Gemini code
review, the mirror uses the existing save_key_file (atomic_write with
0o600 permissions + fsync) and ensure_key_dir (0o700 on parent) helpers
to keep file permissions restrictive and the write race-free. A second
optimization (also per Gemini) only invokes the fsync when the file
content does not already match the keyring key, eliminating per-invocation
disk wear on the common case. Error messages are sanitized via
sanitize_for_terminal. This matches the function docstring's design
intent and prevents the silent-wipe bug.
Tests (29/29 pass):
- Renamed 'cleans_up_legacy_file_*' to 'mirrors_legacy_file_*' to
reflect the new behavior
- Updated assertions: file now exists after the operation and contains
the keyring key
- New: 'keyring_backend_skips_write_when_file_already_has_correct_key'
verifies the fsync is skipped when the file matches (via mtime check)
- New: 'keyring_backend_writes_when_file_has_stale_key' verifies the
file IS updated when the content doesn't match
- Linux fallback tests already mirrored this behavior; macOS/Windows
tests now match
Reported-by: Alvin Chang (alvin@goodciso.org)
Original-patch-author: Cole (AI closer agent, goodciso.org)
Tested-by: Cole (Cole, 2026年06月12日)
Co-authored-by: Cole <cole-closer@goodciso.org>
Co-authored-by: Alvin Chang <alvin.chang@gmail.com>
fcc2e91 to
b9c028a
Compare
googleworkspace-bot
commented
Jun 12, 2026
/gemini review
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request fixes a silent authentication wipe issue by mirroring the OS keyring key to the .encryption_key fallback file instead of deleting it. It introduces a check to only write to the file when the existing content differs from the keyring key, preventing unnecessary disk writes. Corresponding unit tests have been updated and added to verify this mirroring and optimization behavior. No review comments were provided, so there is no feedback to address.
Uh oh!
There was an error while loading. Please reload this page.
Summary
The
resolve_key()function incredential_store.rshad a TOCTOU bug where theKeyringBackend::Keyringpath would auto-delete the.encryption_keyfallback file when a key was found in the OS keyring. The function docstring explicitly stated the file should never be deleted, but the code contradicted the doc.This caused a silent auth wipe: when a user switched from
GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND=fileback to the defaultkeyringbackend, the keyring would return a stale entry from before the file backend was used, then delete the.encryption_keyfile (which contained the actual current key). The now-undecryptablecredentials.enctriggered a second cleanup that deleted the credentials themselves — wiping the user's authenticated session without warning.The fix mirrors the keyring key to the
.encryption_keyfile (keeping the file in sync with the keyring) instead of deleting it. This matches the function docstring's design intent and prevents the silent-wipe bug.Reproduction
gws auth loginwithGOOGLE_WORKSPACE_CLI_KEYRING_BACKEND=file→.encryption_keyandcredentials.enccreatedgws auth login(default = keyring) → keyring returns stale entry, deletes.encryption_keygwscall → credentials.enc undecryptable → auto-cleanup deletes itFix
The
.encryption_keyfile is now always present and always matches the keyring.credentials.encis always decryptable. No silent wipes.Tests
keyring_backend_cleans_up_legacy_file_*→keyring_backend_mirrors_legacy_file_*to reflect the new behaviorcredential_storemodule pass; 0 failAttribution
gwsauth state with 195 sends / 0 replies was finally diagnosed. Root cause was missing DKIM/DMARC at goodciso.org, but the auth flow was independently broken by this bug. Reproduced locally and patched.The commit includes
Co-authored-by:trailers for both contributors. (GitHub renders the commit message in the PR's commits tab; the trailers ensure both names appear in the contributors graph.)