Skip to content

Commit e380be2

Browse files
committed
fix(secrets): scope longer keyring timeout to macOS
1 parent 8d9de2d commit e380be2

4 files changed

Lines changed: 36 additions & 25 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Added
66

7+
- Auth: make keyring open/operation timeouts configurable with `GOG_KEYRING_OPEN_TIMEOUT`, using a `30s` macOS default for permission prompts while retaining `10s` elsewhere. (#845) — thanks @malob.
78
- Calendar: add `create --timezone`/`--tz` to apply one IANA timezone to both event endpoints while retaining granular start/end timezone flags. (#844) — thanks @malob.
89
- Docs: add non-destructive `insert-image --before` and `--after` anchors while clarifying that `--at` replaces its placeholder. (#839) — thanks @sebsnyk.
910
- Slides: allow `insert-image` and `replace-slide` to use public HTTPS image URLs without temporary Drive sharing. (#825) — thanks @sebsnyk.

docs/spec.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ Implementation: `internal/config/*`.
112112
- Legacy key format: `token:<email>` (migrated on first read)
113113
- Stored payload is JSON (refresh token + metadata like OIDC subject, current email, selected services/scopes).
114114
- Email is treated as display/contact state; Google's OIDC `sub` is used to detect the same account after an email rename and migrate aliases/defaults/client mappings on reauthorization.
115-
- macOS Keychain operations are bounded by a timeout (default `30s`, configurable via `GOG_KEYRING_OPEN_TIMEOUT`) so non-surfacing permission prompts return actionable guidance instead of hanging indefinitely.
115+
- Keyring operations are bounded by a timeout (default `30s` on macOS and `10s` elsewhere, configurable via `GOG_KEYRING_OPEN_TIMEOUT`) so non-surfacing permission prompts and unresponsive backends return actionable guidance instead of hanging indefinitely.
116116
- Fallback: if no OS credential store is available, keyring may use its encrypted "file" backend:
117117
- Directory: `$(os.UserConfigDir())/gogcli/keyring/` (one file per key; gog-managed key names are encoded for portable filenames)
118118
- Password: prompts on TTY; for non-interactive runs set `GOG_KEYRING_PASSWORD`
@@ -169,7 +169,7 @@ Environment:
169169
- `GOG_KEYRING_PASSWORD=...` (used when keyring falls back to encrypted file backend in non-interactive environments)
170170
- `GOG_KEYRING_BACKEND={auto|keychain|file}` (force backend; use `file` to avoid Keychain prompts and pair with `GOG_KEYRING_PASSWORD` for non-interactive)
171171
- `GOG_KEYRING_SERVICE_NAME=...` (override keyring namespace/service name; default `gogcli`)
172-
- `GOG_KEYRING_OPEN_TIMEOUT=30s` (max time to wait for a keyring open/operation — e.g. a macOS Keychain permission prompt — before failing; Go duration, default `30s`)
172+
- `GOG_KEYRING_OPEN_TIMEOUT=30s` (max time to wait for a keyring open/operation — e.g. a macOS Keychain permission prompt — before failing; Go duration, default `30s` on macOS and `10s` elsewhere)
173173
- `GOG_TIMEZONE=America/New_York` (default output timezone; IANA name or `UTC`; `local` forces local timezone)
174174
- `GOG_ENABLE_COMMANDS=calendar,tasks,gmail.search` (optional prefix allowlist; dot paths allowed; parent paths allow children)
175175
- `GOG_ENABLE_COMMANDS_EXACT=calendar.events,gmail.search` (optional exact allowlist; dot paths allowed; parent paths do not allow children)

internal/secrets/backend.go

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ func OpenOptionsFromLookup(
8282
GOOS: goos,
8383
DBusAddress: dbusAddress,
8484
IsTTY: isTTY,
85-
OpenTimeout: parseKeyringOpenTimeout(openTimeoutRaw),
85+
OpenTimeout: parseKeyringOpenTimeout(openTimeoutRaw, goos),
8686
LockTimeout: parseKeyringLockTimeout(lockTimeoutRaw),
8787
}
8888
}
@@ -163,28 +163,34 @@ func serviceNameFor(options OpenOptions) string {
163163
return config.AppName
164164
}
165165

166-
// keyringOpenTimeout is the default maximum time to wait for a keyring open or
167-
// operation to complete; override with GOG_KEYRING_OPEN_TIMEOUT. It guards
168-
// against backends that can hang indefinitely (headless-Linux D-Bus
169-
// SecretService when gnome-keyring is installed but not running; an unresponsive
170-
// macOS Keychain) while still leaving room for an interactive macOS Keychain
171-
// permission prompt (password entry plus "Always Allow").
166+
// Keyring timeouts guard against unresponsive backends. macOS gets longer for
167+
// interactive permission prompts; other platforms retain the existing limit.
172168
const (
173-
keyringOpenTimeout = 30 * time.Second
174-
goosDarwin = "darwin"
175-
goosLinux = "linux"
169+
keyringOpenTimeout = 10 * time.Second
170+
darwinKeyringOpenTimeout = 30 * time.Second
171+
goosDarwin = "darwin"
172+
goosLinux = "linux"
176173
)
177174

178-
// parseKeyringOpenTimeout resolves GOG_KEYRING_OPEN_TIMEOUT, falling back to the
179-
// default when unset, unparseable, or non-positive.
180-
func parseKeyringOpenTimeout(raw string) time.Duration {
175+
func defaultKeyringOpenTimeout(goos string) time.Duration {
176+
if goos == goosDarwin {
177+
return darwinKeyringOpenTimeout
178+
}
179+
180+
return keyringOpenTimeout
181+
}
182+
183+
// parseKeyringOpenTimeout resolves GOG_KEYRING_OPEN_TIMEOUT, falling back to
184+
// the platform default when unset, unparseable, or non-positive.
185+
func parseKeyringOpenTimeout(raw, goos string) time.Duration {
186+
fallback := defaultKeyringOpenTimeout(goos)
181187
if raw == "" {
182-
return keyringOpenTimeout
188+
return fallback
183189
}
184190

185191
timeout, err := time.ParseDuration(raw)
186192
if err != nil || timeout <= 0 {
187-
return keyringOpenTimeout
193+
return fallback
188194
}
189195

190196
return timeout
@@ -269,7 +275,7 @@ func openKeyringWithOptions(options OpenOptions) (keyring.Keyring, error) {
269275

270276
openTimeout := options.OpenTimeout
271277
if openTimeout <= 0 {
272-
openTimeout = keyringOpenTimeout
278+
openTimeout = defaultKeyringOpenTimeout(options.GOOS)
273279
}
274280

275281
open := options.openKeyringFn
@@ -315,7 +321,7 @@ func prepareKeyring(
315321
if shouldUseKeyringOperationTimeout(options.GOOS, backendInfo, options.DBusAddress) {
316322
timeout := options.OpenTimeout
317323
if timeout <= 0 {
318-
timeout = keyringOpenTimeout
324+
timeout = defaultKeyringOpenTimeout(options.GOOS)
319325
}
320326
ring = newTimeoutKeyring(ring, timeout, keyringTimeoutHint(options.GOOS))
321327
}

internal/secrets/store_test.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -102,20 +102,24 @@ func TestParseKeyringOpenTimeout(t *testing.T) {
102102
tests := []struct {
103103
name string
104104
raw string
105+
goos string
105106
want time.Duration
106107
}{
107-
{name: "empty falls back to default", raw: "", want: keyringOpenTimeout},
108-
{name: "valid duration", raw: "1m", want: time.Minute},
109-
{name: "unparseable falls back to default", raw: "nonsense", want: keyringOpenTimeout},
110-
{name: "non-positive falls back to default", raw: "-5s", want: keyringOpenTimeout},
108+
{name: "darwin default", goos: "darwin", want: darwinKeyringOpenTimeout},
109+
{name: "linux default", goos: "linux", want: keyringOpenTimeout},
110+
{name: "other default", goos: "windows", want: keyringOpenTimeout},
111+
{name: "valid duration overrides darwin", raw: "1m", goos: "darwin", want: time.Minute},
112+
{name: "valid duration overrides linux", raw: "45s", goos: "linux", want: 45 * time.Second},
113+
{name: "invalid uses darwin default", raw: "nonsense", goos: "darwin", want: darwinKeyringOpenTimeout},
114+
{name: "non-positive uses linux default", raw: "-5s", goos: "linux", want: keyringOpenTimeout},
111115
}
112116

113117
for _, tt := range tests {
114118
t.Run(tt.name, func(t *testing.T) {
115119
t.Parallel()
116120

117-
if got := parseKeyringOpenTimeout(tt.raw); got != tt.want {
118-
t.Fatalf("parseKeyringOpenTimeout(%q) = %v, want %v", tt.raw, got, tt.want)
121+
if got := parseKeyringOpenTimeout(tt.raw, tt.goos); got != tt.want {
122+
t.Fatalf("parseKeyringOpenTimeout(%q, %q) = %v, want %v", tt.raw, tt.goos, got, tt.want)
119123
}
120124
})
121125
}

0 commit comments

Comments
 (0)