fix(credential_store): stop persisting encryption key file when keyring is available (#345)

Previously, get_or_create_key() unconditionally wrote the encryption key
to ~/.config/gws/.encryption_key on first run, even when the OS keyring
was available. This left the key material on disk as a plain file,
making credentials portable by copying the config directory.

Changes:
- Extract save_key_file() helper to deduplicate file-writing logic
- On keyring read success: delete stale .encryption_key (migration)
- On NoEntry + existing file: migrate key into keyring, then delete file
- On NoEntry + new key: try keyring first, only write file as fallback

Fixes #344

Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
This commit is contained in:
Justin Poehnelt
2026-03-09 15:51:05 -06:00
committed by GitHub
parent 06edf7b05f
commit 5872dbe474
3 changed files with 83 additions and 63 deletions
@@ -0,0 +1,5 @@
---
"@googleworkspace/cli": patch
---
Stop persisting encryption key to `.encryption_key` file when OS keyring is available. Existing file-based keys are migrated into the keyring and the file is removed on next CLI invocation.
+28 -4
View File
@@ -73,7 +73,13 @@ jobs:
- name: Enable sccache
if: steps.sccache.outcome == 'success'
run: echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV"
shell: bash
run: |
if sccache --start-server 2>/dev/null; then
echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV"
else
echo "::warning::sccache server failed to start, building without cache"
fi
- name: Cache cargo
uses: Swatinem/rust-cache@ad397744b0d591a723ab90405b7247fac0e6b8db # v2
@@ -119,7 +125,13 @@ jobs:
- name: Enable sccache
if: steps.sccache.outcome == 'success'
run: echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV"
shell: bash
run: |
if sccache --start-server 2>/dev/null; then
echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV"
else
echo "::warning::sccache server failed to start, building without cache"
fi
- name: Cache cargo
uses: Swatinem/rust-cache@ad397744b0d591a723ab90405b7247fac0e6b8db # v2
@@ -155,7 +167,13 @@ jobs:
- name: Enable sccache
if: steps.sccache.outcome == 'success'
run: echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV"
shell: bash
run: |
if sccache --start-server 2>/dev/null; then
echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV"
else
echo "::warning::sccache server failed to start, building without cache"
fi
- name: Cache cargo
uses: Swatinem/rust-cache@ad397744b0d591a723ab90405b7247fac0e6b8db # v2
@@ -204,7 +222,13 @@ jobs:
- name: Enable sccache
if: steps.sccache.outcome == 'success'
run: echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV"
shell: bash
run: |
if sccache --start-server 2>/dev/null; then
echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV"
else
echo "::warning::sccache server failed to start, building without cache"
fi
- name: Cache cargo
uses: Swatinem/rust-cache@ad397744b0d591a723ab90405b7247fac0e6b8db # v2
+50 -59
View File
@@ -21,6 +21,38 @@ use keyring::Entry;
use rand::RngCore;
use std::sync::OnceLock;
/// Persist the base64-encoded encryption key to a local file with restrictive
/// permissions (0600 file, 0700 directory). Used only as a fallback when the OS
/// keyring is unavailable.
fn save_key_file(path: &std::path::Path, b64_key: &str) -> std::io::Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Err(e) = std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700))
{
eprintln!("Warning: failed to set secure permissions on key directory: {e}");
}
}
}
#[cfg(unix)]
{
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
let mut options = std::fs::OpenOptions::new();
options.write(true).create(true).truncate(true).mode(0o600);
let mut file = options.open(path)?;
file.write_all(b64_key.as_bytes())?;
}
#[cfg(not(unix))]
{
std::fs::write(path, b64_key)?;
}
Ok(())
}
/// Returns the encryption key derived from the OS keyring, or falls back to a local file.
/// Generates a random 256-bit key and stores it securely if it doesn't exist.
fn get_or_create_key() -> anyhow::Result<[u8; 32]> {
@@ -57,6 +89,11 @@ fn get_or_create_key() -> anyhow::Result<[u8; 32]> {
if decoded.len() == 32 {
let mut arr = [0u8; 32];
arr.copy_from_slice(&decoded);
// Keyring is authoritative — remove redundant file copy
// if it exists (migrates existing installs on upgrade).
if key_file.exists() {
let _ = std::fs::remove_file(&key_file);
}
return Ok(cache_key(arr));
}
}
@@ -71,51 +108,30 @@ fn get_or_create_key() -> anyhow::Result<[u8; 32]> {
if decoded.len() == 32 {
let mut arr = [0u8; 32];
arr.copy_from_slice(&decoded);
// Best effort: repopulate keyring for future runs.
let _ = entry.set_password(&b64_key);
// Migrate file key into keyring; remove the
// file if the keyring store succeeds.
if entry.set_password(b64_key.trim()).is_ok() {
let _ = std::fs::remove_file(&key_file);
}
return Ok(cache_key(arr));
}
}
}
}
// Generate a random 32-byte key and persist it locally as a stable fallback.
// Generate a new random 256-bit key.
let mut key = [0u8; 32];
rand::thread_rng().fill_bytes(&mut key);
let b64_key = STANDARD.encode(key);
if let Some(parent) = key_file.parent() {
let _ = std::fs::create_dir_all(parent);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Err(e) =
std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700))
{
eprintln!(
"Warning: failed to set secure permissions on key directory: {e}"
);
}
}
// Try keyring first; only fall back to file storage
// if the keyring is unavailable.
if entry.set_password(&b64_key).is_ok() {
return Ok(cache_key(key));
}
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
let mut options = std::fs::OpenOptions::new();
options.write(true).create(true).truncate(true).mode(0o600);
if let Ok(mut file) = options.open(&key_file) {
use std::io::Write;
let _ = file.write_all(b64_key.as_bytes());
}
}
#[cfg(not(unix))]
{
let _ = std::fs::write(&key_file, &b64_key);
}
// Best effort: also store in keyring when available.
let _ = entry.set_password(&b64_key);
// Keyring store failed — persist to local file as fallback.
save_key_file(&key_file, &b64_key)?;
return Ok(cache_key(key));
}
@@ -147,32 +163,7 @@ fn get_or_create_key() -> anyhow::Result<[u8; 32]> {
use base64::{engine::general_purpose::STANDARD, Engine as _};
let b64_key = STANDARD.encode(key);
if let Some(parent) = key_file.parent() {
let _ = std::fs::create_dir_all(parent);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Err(e) = std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700))
{
eprintln!("Warning: failed to set secure permissions on key directory: {e}");
}
}
}
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
let mut options = std::fs::OpenOptions::new();
options.write(true).create(true).truncate(true).mode(0o600);
if let Ok(mut file) = options.open(&key_file) {
use std::io::Write;
let _ = file.write_all(b64_key.as_bytes());
}
}
#[cfg(not(unix))]
{
let _ = std::fs::write(&key_file, b64_key);
}
save_key_file(&key_file, &b64_key)?;
Ok(cache_key(key))
}