Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

fix(credential-store): mirror .encryption_key instead of deleting it (fixes silent auth wipe)#845

Open
alvin-chang wants to merge 1 commit into
googleworkspace:main from
alvin-chang:fix/encryption-key-toctou-cleanup
Open

fix(credential-store): mirror .encryption_key instead of deleting it (fixes silent auth wipe) #845
alvin-chang wants to merge 1 commit into
googleworkspace:main from
alvin-chang:fix/encryption-key-toctou-cleanup

Conversation

@alvin-chang

@alvin-chang alvin-chang commented Jun 12, 2026
edited
Loading

Copy link
Copy Markdown

Summary

The resolve_key() function in credential_store.rs had a TOCTOU bug where the KeyringBackend::Keyring path would auto-delete 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, but the code contradicted the doc.

This caused a silent auth wipe: when a user switched from GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND=file back to the default keyring backend, 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). The now-undecryptable credentials.enc triggered 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_key file (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

  1. gws auth login with GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND=file.encryption_key and credentials.enc created
  2. gws auth login (default = keyring) → keyring returns stale entry, deletes .encryption_key
  3. gws call → credentials.enc undecryptable → auto-cleanup deletes it
  4. User is silently logged out

Fix

// Before (auto-delete, contradicts docstring)
if let Err(e) = std::fs::remove_file(key_file) { ... }
// After (mirror — keep file in sync with keyring)
std::fs::write(key_file, b64_key.trim())

The .encryption_key file is now always present and always matches the keyring. credentials.enc is always decryptable. No silent wipes.

Tests

  • Renamed keyring_backend_cleans_up_legacy_file_*keyring_backend_mirrors_legacy_file_* to reflect the new behavior
  • Updated assertions: file now exists after the operation and contains the keyring key
  • 27 tests in credential_store module pass; 0 fail
test result: ok. 27 passed; 0 failed; 0 ignored; 0 measured

Attribution

  • Original patch author: Cole (AI closer agent, goodciso.org) — wrote the fix and tests
  • Reviewed and committed by: Alvin Chang (alvin@goodciso.org) — CEO of Good CISO
  • Discovered in production by: Alvin Chang on 2026年06月12日 when a 4-day-old gws auth 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.)

gemini-code-assist[bot] reacted with eyes emoji

changeset-bot Bot commented Jun 12, 2026
edited
Loading

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b9c028a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@googleworkspace/cli Patch

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

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, 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 .encryption_key fallback file upon successful keyring access. By switching from a deletion-based cleanup to a mirroring strategy, the system ensures that the file-based fallback remains consistent with the OS keyring, preventing accidental credential loss during backend switching or environment transitions.

Highlights

  • Credential Store Logic: Modified the resolve_key function to mirror the keyring key to the .encryption_key file instead of deleting it, ensuring a durable fallback for headless or CI environments.
  • Bug Prevention: Addressed a silent authentication wipe issue where stale keyring entries caused the deletion of the active encryption key, rendering credentials undecryptable.
  • Test Suite Updates: Updated existing tests to verify that the legacy key file is correctly synchronized with the keyring rather than removed.
Using Gemini Code Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Generative AI Prohibited Use Policy, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

google-cla Bot commented Jun 12, 2026

Copy link
Copy Markdown

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.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

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.

Comment on lines 216 to 225
if let Some(parent) = key_file.parent() {
let _ = std::fs::create_dir_all(parent);
}
if let Err(e) = std::fs::write(key_file, b64_key.trim()) {
eprintln!(
"Warning: failed to mirror keyring key to '{}': {}",
key_file.display(),
e
);
}

@gemini-code-assist gemini-code-assist Bot Jun 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

security-high high

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
 );
 }

Comment on lines 251 to 260
if let Some(parent) = key_file.parent() {
let _ = std::fs::create_dir_all(parent);
}
if let Err(e) = std::fs::write(key_file, b64_key.trim()) {
eprintln!(
"Warning: failed to mirror keyring key to '{}': {}",
key_file.display(),
e
);
}

@gemini-code-assist gemini-code-assist Bot Jun 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

security-high high

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
 );
 }

@alvin-chang alvin-chang force-pushed the fix/encryption-key-toctou-cleanup branch from b887cf0 to be21424 Compare June 12, 2026 18:48

Copy link
Copy Markdown
Collaborator

/gemini review

gemini-code-assist[bot] reacted with eyes emoji

@alvin-chang alvin-chang force-pushed the fix/encryption-key-toctou-cleanup branch from be21424 to 12c9e32 Compare June 12, 2026 18:52

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

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.

Comment on lines +216 to +225
// Uses save_key_file (atomic_write with 0o600 + fsync) and
// ensure_key_dir (0o700 on parent) to keep file permissions
// restrictive and the write race-free.
if let Err(e) = ensure_key_dir(key_file) {
eprintln!(
"Warning: failed to set up key directory '{}': {}",
key_file.display(),
e
);
}

@gemini-code-assist gemini-code-assist Bot Jun 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

critical

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_dir to create the directory with secure permissions (0700).
  • save_key_file which performs an atomic write.
  • sanitize_for_terminal on 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
  1. Sanitize error strings printed to the terminal to prevent escape sequence injection.

Copy link
Copy Markdown
Collaborator

/gemini review

gemini-code-assist[bot] reacted with eyes emoji

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

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.

Comment on lines 219 to 232
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
);
}

@gemini-code-assist gemini-code-assist Bot Jun 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

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
 );
 }
 }

@alvin-chang alvin-chang force-pushed the fix/encryption-key-toctou-cleanup branch from 2cf2292 to b64509e Compare June 12, 2026 18:58

Copy link
Copy Markdown
Collaborator

/gemini review

gemini-code-assist[bot] reacted with eyes emoji

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

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.

@alvin-chang alvin-chang force-pushed the fix/encryption-key-toctou-cleanup branch from b64509e to 65655d8 Compare June 12, 2026 19:49

Copy link
Copy Markdown
Collaborator

/gemini review

gemini-code-assist[bot] reacted with eyes emoji

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

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.

Comment on lines +227 to 240
if let Err(e) = ensure_key_dir(key_file) {
eprintln!(
"Warning: failed to remove legacy key file at '{}': {}",
"Warning: failed to set up key directory '{}': {}",
key_file.display(),
e
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())
);
}

@gemini-code-assist gemini-code-assist Bot Jun 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

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())
 );
 }
 }

Comment on lines +278 to 291
if let Err(e) = ensure_key_dir(key_file) {
eprintln!(
"Warning: failed to remove legacy key file at '{}': {}",
"Warning: failed to set up key directory '{}': {}",
key_file.display(),
e
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())
);
}

@gemini-code-assist gemini-code-assist Bot Jun 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

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())
 );
 }
 }

@alvin-chang alvin-chang force-pushed the fix/encryption-key-toctou-cleanup branch from 65655d8 to e53c061 Compare June 12, 2026 19:57

Copy link
Copy Markdown
Collaborator

/gemini review

gemini-code-assist[bot] reacted with eyes emoji

@alvin-chang alvin-chang force-pushed the fix/encryption-key-toctou-cleanup branch from e53c061 to fcc2e91 Compare June 12, 2026 19:59

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

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.

Comment on lines +227 to 240
if let Err(e) = ensure_key_dir(key_file) {
eprintln!(
"Warning: failed to remove legacy key file at '{}': {}",
"Warning: failed to set up key directory '{}': {}",
key_file.display(),
e
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())
);
}

@gemini-code-assist gemini-code-assist Bot Jun 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

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())
 );
 }

Comment on lines +278 to 291
if let Err(e) = ensure_key_dir(key_file) {
eprintln!(
"Warning: failed to remove legacy key file at '{}': {}",
"Warning: failed to set up key directory '{}': {}",
key_file.display(),
e
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())
);
}

@gemini-code-assist gemini-code-assist Bot Jun 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

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())
 );
 }

Copy link
Copy Markdown
Collaborator

/gemini review

gemini-code-assist[bot] reacted with eyes emoji

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

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.

Comment on lines +227 to 240
if let Err(e) = ensure_key_dir(key_file) {
eprintln!(
"Warning: failed to remove legacy key file at '{}': {}",
"Warning: failed to set up key directory '{}': {}",
key_file.display(),
e
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())
);
}

@gemini-code-assist gemini-code-assist Bot Jun 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

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())
 );
 }

Comment on lines +278 to 291
if let Err(e) = ensure_key_dir(key_file) {
eprintln!(
"Warning: failed to remove legacy key file at '{}': {}",
"Warning: failed to set up key directory '{}': {}",
key_file.display(),
e
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())
);
}

@gemini-code-assist gemini-code-assist Bot Jun 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

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>
@alvin-chang alvin-chang force-pushed the fix/encryption-key-toctou-cleanup branch from fcc2e91 to b9c028a Compare June 12, 2026 20:07

Copy link
Copy Markdown
Collaborator

/gemini review

gemini-code-assist[bot] reacted with eyes emoji

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

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.

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

Reviewers

1 more reviewer

@gemini-code-assist gemini-code-assist[bot] gemini-code-assist[bot] left review comments

Reviewers whose approvals may not affect merge requirements

At least 0 approving reviews are required to merge this pull request.

Assignees

No one assigned

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

AltStyle によって変換されたページ (->オリジナル) /