Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .changeset/31852fab9121.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"@googleworkspace/cli": patch
---

fix(auth): stabilize encrypted credential key fallback across sessions

When the OS keyring returned `NoEntry`, the previous code could generate
a fresh random key on each process invocation instead of reusing one.
This caused `credentials.enc` written by `gws auth login` to be
unreadable by subsequent commands.

Changes:
- Always prefer an existing `.encryption_key` file before generating a new key
- When generating a new key, persist it to `.encryption_key` as a stable fallback
- Best-effort write new keys into the keyring as well
- Fix `OnceLock` race: return the already-cached key if `set` loses a race

Fixes #27
86 changes: 72 additions & 14 deletions src/credential_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,23 @@ fn get_or_create_key() -> anyhow::Result<[u8; 32]> {
return Ok(*key);
}

let cache_key = |candidate: [u8; 32]| -> [u8; 32] {
if KEY.set(candidate).is_ok() {
candidate
} else {
// If set() fails, another thread already initialized the key. .get() is
// guaranteed to return Some at this point.
*KEY.get()
.expect("key must be initialized if OnceLock::set() failed")
}
};

let username = std::env::var("USER")
.or_else(|_| std::env::var("USERNAME"))
.unwrap_or_else(|_| "unknown-user".to_string());

let key_file = crate::auth_commands::config_dir().join(".encryption_key");

let entry = Entry::new("gws-cli", &username);

if let Ok(entry) = entry {
Expand All @@ -44,39 +57,82 @@ fn get_or_create_key() -> anyhow::Result<[u8; 32]> {
if decoded.len() == 32 {
let mut arr = [0u8; 32];
arr.copy_from_slice(&decoded);
let _ = KEY.set(arr);
return Ok(arr);
return Ok(cache_key(arr));
}
}
}
Err(keyring::Error::NoEntry) => {
// Generate a random 32-byte key
use base64::{engine::general_purpose::STANDARD, Engine as _};

// If keyring is empty, prefer a persisted local key first.
if key_file.exists() {
if let Ok(b64_key) = std::fs::read_to_string(&key_file) {
if let Ok(decoded) = STANDARD.decode(b64_key.trim()) {
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);
return Ok(cache_key(arr));
}
}
}
}

// Generate a random 32-byte key and persist it locally as a stable fallback.
let mut key = [0u8; 32];
rand::thread_rng().fill_bytes(&mut key);

use base64::{engine::general_purpose::STANDARD, Engine as _};
let b64_key = STANDARD.encode(key);

if entry.set_password(&b64_key).is_ok() {
let _ = KEY.set(key);
return Ok(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);
}

// Best effort: also store in keyring when available.
let _ = entry.set_password(&b64_key);

return Ok(cache_key(key));
}
Err(_) => {} // Fallthrough to file storage
}
}

// Fallback: Local file `.encryption_key`
let key_file = crate::auth_commands::config_dir().join(".encryption_key");

if key_file.exists() {
if let Ok(b64_key) = std::fs::read_to_string(&key_file) {
use base64::{engine::general_purpose::STANDARD, Engine as _};
if let Ok(decoded) = STANDARD.decode(b64_key.trim()) {
if decoded.len() == 32 {
let mut arr = [0u8; 32];
arr.copy_from_slice(&decoded);
let _ = KEY.set(arr);
return Ok(arr);
return Ok(cache_key(arr));
}
}
}
Expand All @@ -94,7 +150,10 @@ fn get_or_create_key() -> anyhow::Result<[u8; 32]> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700));
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}");
}
}
}

Expand All @@ -113,8 +172,7 @@ fn get_or_create_key() -> anyhow::Result<[u8; 32]> {
let _ = std::fs::write(&key_file, b64_key);
}

let _ = KEY.set(key);
Ok(key)
Ok(cache_key(key))
}

/// Encrypts plaintext bytes using AES-256-GCM with a machine-derived key.
Expand Down
Loading