(options OpenOptions)
| 232 | } |
| 233 | |
| 234 | func openKeyringWithOptions(options OpenOptions) (keyring.Keyring, error) { |
| 235 | // On Linux/WSL/containers, OS keychains (secret-service/kwallet) may be unavailable. |
| 236 | // In that case github.com/99designs/keyring falls back to the "file" backend, |
| 237 | // which *requires* both a directory and a password prompt function. |
| 238 | keyringDir, err := options.Layout.EnsureKeyringDir() |
| 239 | if err != nil { |
| 240 | return nil, fmt.Errorf("ensure keyring dir: %w", err) |
| 241 | } |
| 242 | |
| 243 | backendInfo, err := ResolveKeyringBackendInfoWithOptions(options) |
| 244 | if err != nil { |
| 245 | return nil, err |
| 246 | } |
| 247 | |
| 248 | backends, err := allowedBackends(backendInfo) |
| 249 | if err != nil { |
| 250 | return nil, err |
| 251 | } |
| 252 | wrapFileKeys := fileKeyringBackendOnly(backends) |
| 253 | |
| 254 | // On Linux with "auto" backend and no D-Bus session, force file backend. |
| 255 | // Without DBUS_SESSION_BUS_ADDRESS, SecretService will hang indefinitely |
| 256 | // trying to connect (common on headless systems like Raspberry Pi). |
| 257 | if shouldForceFileBackend(options.GOOS, backendInfo, options.DBusAddress) { |
| 258 | backends = []keyring.BackendType{keyring.FileBackend} |
| 259 | wrapFileKeys = true |
| 260 | } |
| 261 | |
| 262 | cfg := keyring.Config{ |
| 263 | ServiceName: serviceNameFor(options), |
| 264 | // KeychainTrustApplication is intentionally false to support Homebrew upgrades. |
| 265 | // When true, macOS Keychain ties access control to the specific binary hash. |
| 266 | // Homebrew upgrades install a new binary with a different hash, causing the |
| 267 | // new binary to lose access to existing keychain items. With false, users may |
| 268 | // see a one-time keychain prompt after upgrade (click "Always Allow"), but |
| 269 | // tokens survive across upgrades. See: https://github.com/steipete/gogcli/issues/86 |
| 270 | KeychainTrustApplication: false, |
| 271 | AllowedBackends: backends, |
| 272 | FileDir: keyringDir, |
| 273 | FilePasswordFunc: fileKeyringPasswordFuncFrom(options.Password, options.PasswordSet, options.IsTTY), |
| 274 | } |
| 275 | |
| 276 | openTimeout := options.OpenTimeout |
| 277 | if openTimeout <= 0 { |
| 278 | openTimeout = defaultKeyringOpenTimeout(options.GOOS) |
| 279 | } |
| 280 | |
| 281 | open := options.openKeyringFn |
| 282 | if open == nil { |
| 283 | open = keyring.Open |
| 284 | } |
| 285 | |
| 286 | // On Linux with D-Bus present, keyring.Open() can still hang if SecretService |
| 287 | // is unresponsive (e.g., gnome-keyring installed but not running). |
| 288 | // Use a timeout as a safety net. |
| 289 | if shouldUseKeyringTimeout(options.GOOS, backendInfo, options.DBusAddress) { |
| 290 | timeoutRing, timeoutErr := openKeyringWithTimeoutFunc( |
| 291 | cfg, |
no test coverage detected