-
Notifications
You must be signed in to change notification settings - Fork 801
Description
Summary
get_or_create_key() in credential_store.rs deletes the file-based encryption key after "migrating" it to the OS keyring, but the keyring entry is not readable by subsequent processes. This causes all encrypted credentials to become permanently undecryptable until the user discovers the workaround.
Affected versions
- gws 0.9.1 (npm-installed binary, ad-hoc signed)
- macOS (Darwin 24.5.0, Apple Silicon)
Symptoms
gws drive files list --params '{"pageSize": 10}'
{
"error": {
"code": 401,
"message": "Authentication failed: Failed to decrypt credentials: Decryption failed. Credentials may have been created on a different machine. Run `gws auth logout` and `gws auth login` to re-authenticate.",
"reason": "authError"
}
}
gws auth loginsucceeds, but any subsequent command fails with the above errorgws auth statusshowsencryption_valid: false- Running
gws auth logout+gws auth logindoes not fix the issue (it recreates the same broken cycle)
Root cause
In src/credential_store.rs lines 104-115, when the keyring has no entry but a .encryption_key file exists:
// 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);
// 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); // <-- deletes the only working copy
}
return Ok(cache_key(arr));
}
}
}
}The problem:
entry.set_password()returnsOk(())— the write appears to succeed- The file is deleted because
set_passwordreturnedOk - The key lives in the
OnceLockfor the remainder of this process (login works) - On the next process invocation,
entry.get_password()fails to find the keyring entry (likely due to macOS Security framework ACL restrictions on ad-hoc signed binaries) - The file is gone, so a new random key is generated
- The new key cannot decrypt the existing
credentials.enc
This creates an unrecoverable loop: every login encrypts with a key that is lost when the process exits.
Reproduction steps
- Install gws via npm on macOS
- Run
gws auth login --account user@example.com— succeeds - Run
gws drive files list --params '{"pageSize": 10}'— fails with 401 decryption error - Observe that
~/.config/gws/.encryption_keydoes not exist - Observe that
security find-generic-password -s "gws-cli" -a "$USER" -wreturns "item not found"
Workaround
Create the encryption key file and make it immutable so the migration cannot delete it:
gws auth logout
python3 -c "import base64, os; print(base64.b64encode(os.urandom(32)).decode())" > ~/.config/gws/.encryption_key
chmod 600 ~/.config/gws/.encryption_key
chflags uchg ~/.config/gws/.encryption_key
gws auth login --account user@example.comSuggested fix
Verify the key is actually readable from the keyring before deleting the file:
if entry.set_password(b64_key.trim()).is_ok() {
// Verify the roundtrip before removing the file fallback
if entry.get_password().is_ok() {
let _ = std::fs::remove_file(&key_file);
}
}The same pattern should be applied to the new-key generation path (lines 127-131) where set_password success skips the file fallback entirely — it should also verify readability before relying on keyring-only storage.