Skip to content

fix(credential_store): preserve .encryption_key file for keyring-less environments#357

Closed
jpoehnelt wants to merge 2 commits intomainfrom
fix/keyring-fallback-regression
Closed

fix(credential_store): preserve .encryption_key file for keyring-less environments#357
jpoehnelt wants to merge 2 commits intomainfrom
fix/keyring-fallback-regression

Conversation

@jpoehnelt
Copy link
Member

Summary

The fix for #344 introduced automatic deletion of .encryption_key when the OS keyring succeeds. In Docker/ephemeral-keyring environments, this permanently loses the encryption key on container restart, making credentials.enc undecryptable.

Changes

  • Never delete .encryption_key — the file persists as a safe fallback. The keyring remains authoritative when available.
  • Always save new keys to file, even when keyring set_password succeeds. This ensures the key survives keyring loss (e.g. Docker container restart).
  • Extract resolve_key() with a KeyringProvider trait for testability.
  • Add 13 new unit tests covering all keyring/file scenarios (keyring ok, NoEntry, platform error, with/without file, set success/failure).

Root Cause

The NoEntry + existing file path (line 113-114) migrated the file key into the keyring, then deleted the file. In Docker, set_password() can succeed within a session but the keyring doesn't persist across container restarts. On next start, both the keyring and the file are gone → key lost forever.

Test Results

test result: ok. 21 passed; 0 failed; 0 ignored (credential_store tests)
test result: ok. 518 passed; 0 failed; 0 ignored (full suite)

Fixes #344 (regression)

… environments

The fix for #344 introduced automatic deletion of .encryption_key when
the OS keyring succeeds. In Docker/ephemeral-keyring environments, this
permanently loses the encryption key on container restart.

Changes:
- Never delete .encryption_key — it persists as a safe fallback
- Always save new keys to file, even when keyring set_password succeeds
- Extract resolve_key() with KeyringProvider trait for testability
- Add 13 new unit tests covering all keyring/file scenarios

Fixes #344 (regression)
@changeset-bot
Copy link

changeset-bot bot commented Mar 10, 2026

🦋 Changeset detected

Latest commit: 5e6070a

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

@gemini-code-assist
Copy link
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 addresses a critical regression that caused encryption keys to be permanently lost in environments where OS keyrings are not persistent, such as Docker containers. The changes ensure that the .encryption_key file is always preserved as a durable fallback, even when keyring operations succeed, thereby preventing data loss and improving the overall robustness of credential management.

Highlights

  • Key Persistence Fix: Prevented the accidental deletion of the .encryption_key file, ensuring encryption keys are not lost in environments with ephemeral keyrings like Docker.
  • Refactored Key Resolution: Introduced a KeyringProvider trait and extracted the core key resolution logic into a new resolve_key function, improving modularity and testability.
  • Enhanced Test Coverage: Added 13 new unit tests to comprehensively cover various keyring and file interaction scenarios, including success, no entry, platform errors, and file existence.
Changelog
  • .changeset/fix-keyring-fallback.md
    • Added a new changeset entry documenting the fix for credential key loss in keyring-less environments.
  • src/credential_store.rs
    • Refactored the get_or_create_key function to introduce a KeyringProvider trait and a new resolve_key function for clearer logic and testability.
    • Modified key resolution to never delete the .encryption_key file, ensuring it acts as a persistent fallback.
    • Ensured new keys are always saved to the local file, even if keyring storage is successful, to protect against keyring loss.
    • Implemented a MockKeyring and added 13 new unit tests to validate key resolution logic across various scenarios.
Activity
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 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 counter productive. 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.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

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.

@googleworkspace-bot googleworkspace-bot added the cla: yes This human has signed the Contributor License Agreement. label Mar 10, 2026
@google-cla google-cla bot added cla: no This human has *not* signed the Contributor License Agreement. and removed cla: yes This human has signed the Contributor License Agreement. labels Mar 10, 2026
@jpoehnelt
Copy link
Member Author

Superseded — switching to a gogcli-style backend selection pattern with env var control instead of removing the keyring entirely.

@jpoehnelt jpoehnelt closed this Mar 10, 2026
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a 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 correctly addresses a regression where the encryption key could be lost in ephemeral environments like Docker. The refactoring to introduce a KeyringProvider trait and add comprehensive unit tests is excellent. I've found one potential race condition during initial key generation that could lead to data loss, and have provided a suggestion to resolve it.

Comment on lines +152 to +155
let key = generate_random_key();
let b64_key = STANDARD.encode(key);
save_key_file(key_file, &b64_key)?;
Ok(key)
Copy link
Contributor

Choose a reason for hiding this comment

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

high

This key generation logic has a race condition when multiple processes run for the first time simultaneously, which can lead to unrecoverable encrypted data.

For example:

  1. Process A generates key_A.
  2. Process B generates key_B.
  3. Process A writes key_A to .encryption_key.
  4. Process B writes key_B to .encryption_key, overwriting key_A.

If Process A has already used key_A to encrypt data, that data is now lost.

To fix this, you can generate a key, write it, and then immediately re-read the file. This ensures all processes converge on using the single key that 'won' the race and was persisted to disk.

Note: This fix assumes that save_key_file performs an atomic write (e.g., using fs_util::atomic_write) to prevent reading a partially written file. The current implementation of save_key_file is not atomic and should also be updated to prevent data corruption on crashes.

Suggested change
let key = generate_random_key();
let b64_key = STANDARD.encode(key);
save_key_file(key_file, &b64_key)?;
Ok(key)
let key_candidate = generate_random_key();
let b64_key = STANDARD.encode(key_candidate);
save_key_file(key_file, &b64_key)?;
// Re-read the key from disk to safely handle concurrent initializations.
// This ensures all processes converge on the key that won the race.
read_key_file(key_file)
.ok_or_else(|| anyhow::anyhow!("Failed to read key file after writing."))

@github-actions github-actions bot added the gemini: reviewed Gemini Code Assist has reviewed the latest changes label Mar 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: auth cla: no This human has *not* signed the Contributor License Agreement. gemini: reviewed Gemini Code Assist has reviewed the latest changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

credential_store: .encryption_key file persists on disk even when OS keyring succeeds

3 participants