Files
gitea-tea/modules/credstore/keyring_store.go
T
Bo-Yi Wu f6d939a8df refactor(credstore): embed credential store and drop sdk-go dependency
- Embed the minimal credstore subset used by tea (SecureStore,
  EncryptedFileStore, KeyringStore, FileStore) as modules/credstore so
  external SDK renames can no longer break the build
- Keep the on-disk format fully compatible: AES-256-GCM values with the
  v1: prefix, credentials.json / credentials.json.enc paths, and the
  Token JSON field names are unchanged, verified by a ciphertext fixture
  generated with sdk-go v1.1.0
- Store the keyring master key under a tea-owned account name
- Reuse the existing kernel-level filelock module instead of the
  upstream lockfile protocol, removing a stale-lock race
- Cover roundtrip, keyring-unavailable fallback, and fixture decryption
  with tests using a mocked keyring
- Remove github.com/go-signet/sdk-go and promote
  github.com/zalando/go-keyring to a direct dependency

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 21:59:07 +08:00

77 lines
2.0 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package credstore
import (
"errors"
"fmt"
"github.com/zalando/go-keyring"
)
// KeyringStore stores values in the OS keyring (macOS Keychain, Linux Secret Service, Windows Credential Manager).
type KeyringStore[T any] struct {
serviceName string
codec Codec[T]
}
// NewKeyringStore creates a new KeyringStore with the given codec.
// Panics if codec is nil.
func NewKeyringStore[T any](serviceName string, codec Codec[T]) *KeyringStore[T] {
if codec == nil {
panic("credstore: NewKeyringStore called with nil codec")
}
return &KeyringStore[T]{serviceName: serviceName, codec: codec}
}
// Load loads data from the keyring for the given client ID.
func (k *KeyringStore[T]) Load(clientID string) (T, error) {
var zero T
data, err := keyring.Get(k.serviceName, clientID)
if err != nil {
if errors.Is(err, keyring.ErrNotFound) {
return zero, ErrNotFound
}
return zero, fmt.Errorf("failed to read from keyring: %w", err)
}
decoded, err := k.codec.Decode(data)
if err != nil {
return zero, fmt.Errorf("failed to decode keyring data: %w", err)
}
return decoded, nil
}
// Save saves data to the keyring for the given client ID.
func (k *KeyringStore[T]) Save(clientID string, data T) error {
if clientID == "" {
return ErrEmptyClientID
}
encoded, err := k.codec.Encode(data)
if err != nil {
return fmt.Errorf("failed to encode data for keyring: %w", err)
}
if err := keyring.Set(k.serviceName, clientID, encoded); err != nil {
return fmt.Errorf("failed to save to keyring: %w", err)
}
return nil
}
// Delete removes data for the given client ID from the keyring.
func (k *KeyringStore[T]) Delete(clientID string) error {
err := keyring.Delete(k.serviceName, clientID)
if err != nil && !errors.Is(err, keyring.ErrNotFound) {
return fmt.Errorf("failed to delete from keyring: %w", err)
}
return nil
}
// String returns a description of this store.
func (k *KeyringStore[T]) String() string {
return "keyring: " + k.serviceName
}