Compare commits
2 Commits
main
...
fix/issue-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04b66fbdee | ||
|
|
81ded37565 |
@ -30,6 +30,7 @@
|
||||
- Gmail: allow Workspace-managed send-as aliases with empty verification status in `send` and `drafts create`. (#407) — thanks @salmonumbrella.
|
||||
- Gmail: preserve the selected `--client` during `watch serve` push handling instead of falling back to the default client. (#411) — thanks @chrysb.
|
||||
- CI: validate release tags and quote the checkout ref in the release workflow to block tag-script injection on manual releases. (#299) — thanks @salmonumbrella.
|
||||
- Secrets: verify keyring token writes by reading them back, so macOS headless Keychain failures return an actionable error instead of silently storing 0 bytes. (#270) — thanks @zerone0x.
|
||||
- Secrets: respect empty `GOG_KEYRING_PASSWORD` (treat set-to-empty as intentional; avoids headless prompts). (#269) — thanks @zerone0x.
|
||||
- Calendar: reject ambiguous calendar-name selectors for `calendar events` instead of guessing. (#131) — thanks @salmonumbrella.
|
||||
- Gmail: `drafts update --quote` now picks a non-draft, non-self message from thread fallback (or errors clearly), avoiding self-quote loops and wrong reply headers. (#394) — thanks @salmonumbrella.
|
||||
|
||||
@ -58,6 +58,7 @@ var (
|
||||
errNoTTY = errors.New("no TTY available for keyring file backend password prompt")
|
||||
errInvalidKeyringBackend = errors.New("invalid keyring backend")
|
||||
errKeyringTimeout = errors.New("keyring connection timed out")
|
||||
errTokenVerifyFailed = errors.New("token verification failed: keyring wrote 0 bytes")
|
||||
openKeyringFunc = openKeyring
|
||||
keyringOpenFunc = keyring.Open
|
||||
)
|
||||
@ -337,10 +338,23 @@ func (s *KeyringStore) SetToken(client string, email string, tok Token) error {
|
||||
return fmt.Errorf("encode token: %w", err)
|
||||
}
|
||||
|
||||
if err := s.ring.Set(keyringItem(tokenKey(normalizedClient, email), payload)); err != nil {
|
||||
primaryKey := tokenKey(normalizedClient, email)
|
||||
if err := s.ring.Set(keyringItem(primaryKey, payload)); err != nil {
|
||||
return wrapKeychainError(fmt.Errorf("store token: %w", err))
|
||||
}
|
||||
|
||||
// Verify the token was actually persisted. On macOS, the Keychain can
|
||||
// silently write 0 bytes when it is locked in a headless/server environment
|
||||
// even though Set returns no error. Read back to catch this.
|
||||
if item, readErr := s.ring.Get(primaryKey); readErr != nil {
|
||||
return fmt.Errorf("%w: could not read back token after write: %w\n\n"+
|
||||
"Workaround: switch to file-based keyring with: gog auth keyring file", errTokenVerifyFailed, readErr)
|
||||
} else if len(item.Data) == 0 {
|
||||
return fmt.Errorf("%w\n\n"+
|
||||
"This usually happens when the macOS Keychain is locked in a headless environment.\n"+
|
||||
"Workaround: switch to file-based keyring with: gog auth keyring file", errTokenVerifyFailed)
|
||||
}
|
||||
|
||||
if normalizedClient == config.DefaultClientName {
|
||||
if err := s.ring.Set(keyringItem(legacyTokenKey(email), payload)); err != nil {
|
||||
return wrapKeychainError(fmt.Errorf("store legacy token: %w", err))
|
||||
|
||||
@ -13,7 +13,10 @@ import (
|
||||
"github.com/steipete/gogcli/internal/config"
|
||||
)
|
||||
|
||||
var errTestKeychain = errors.New("test -25308 error")
|
||||
var (
|
||||
errTestKeychain = errors.New("test -25308 error")
|
||||
errTestReadBack = errors.New("test read-back failure")
|
||||
)
|
||||
|
||||
func TestKeyringStore_ListDeleteDefault(t *testing.T) {
|
||||
ring := keyring.NewArrayKeyring(nil)
|
||||
@ -251,6 +254,65 @@ func TestGetTokenMigrationSetsLabel(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// silentDropKeyring simulates a macOS Keychain that silently writes 0 bytes.
|
||||
// Set returns nil (success), but Get returns an item with empty Data.
|
||||
type silentDropKeyring struct {
|
||||
keyring.ArrayKeyring
|
||||
}
|
||||
|
||||
func (s *silentDropKeyring) Set(_ keyring.Item) error { return nil }
|
||||
func (s *silentDropKeyring) Get(_ string) (keyring.Item, error) {
|
||||
return keyring.Item{Data: nil}, nil
|
||||
}
|
||||
func (s *silentDropKeyring) Keys() ([]string, error) { return nil, nil }
|
||||
|
||||
func TestSetTokenVerifyCatchesEmptyWrite(t *testing.T) {
|
||||
store := &KeyringStore{ring: &silentDropKeyring{}}
|
||||
client := config.DefaultClientName
|
||||
|
||||
err := store.SetToken(client, "a@b.com", Token{RefreshToken: "rt", CreatedAt: time.Now()})
|
||||
if err == nil {
|
||||
t.Fatal("expected error when keyring silently drops data")
|
||||
}
|
||||
|
||||
if !errors.Is(err, errTokenVerifyFailed) {
|
||||
t.Fatalf("expected errTokenVerifyFailed, got: %v", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "gog auth keyring file") {
|
||||
t.Fatalf("expected workaround suggestion in error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// readBackErrorKeyring simulates a keyring where Set succeeds but Get fails.
|
||||
type readBackErrorKeyring struct {
|
||||
keyring.ArrayKeyring
|
||||
}
|
||||
|
||||
func (r *readBackErrorKeyring) Set(_ keyring.Item) error { return nil }
|
||||
func (r *readBackErrorKeyring) Get(_ string) (keyring.Item, error) {
|
||||
return keyring.Item{}, errTestReadBack
|
||||
}
|
||||
func (r *readBackErrorKeyring) Keys() ([]string, error) { return nil, nil }
|
||||
|
||||
func TestSetTokenVerifyCatchesReadBackError(t *testing.T) {
|
||||
store := &KeyringStore{ring: &readBackErrorKeyring{}}
|
||||
client := config.DefaultClientName
|
||||
|
||||
err := store.SetToken(client, "a@b.com", Token{RefreshToken: "rt", CreatedAt: time.Now()})
|
||||
if err == nil {
|
||||
t.Fatal("expected error when keyring read-back fails")
|
||||
}
|
||||
|
||||
if !errors.Is(err, errTokenVerifyFailed) {
|
||||
t.Fatalf("expected errTokenVerifyFailed, got: %v", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "could not read back") {
|
||||
t.Fatalf("expected read-back error detail, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetSecretSetsLabel(t *testing.T) {
|
||||
ring := keyring.NewArrayKeyring(nil)
|
||||
origOpen := openKeyringFunc
|
||||
|
||||
Loading…
Reference in New Issue
Block a user