diff --git a/go.mod b/go.mod index c215315..d2cd1fe 100644 --- a/go.mod +++ b/go.mod @@ -28,11 +28,14 @@ require ( require ( filippo.io/edwards25519 v1.1.0 // indirect + github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect + github.com/99designs/keyring v1.2.2 github.com/ClickHouse/ch-go v0.71.0 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/apache/arrow-go/v18 v18.5.1 // indirect github.com/bep/debounce v1.2.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/danieljoos/wincred v1.1.2 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/duckdb/duckdb-go-bindings v0.3.3 // indirect github.com/duckdb/duckdb-go-bindings/lib/darwin-amd64 v0.3.3 // indirect @@ -41,17 +44,20 @@ require ( github.com/duckdb/duckdb-go-bindings/lib/linux-arm64 v0.3.3 // indirect github.com/duckdb/duckdb-go-bindings/lib/windows-amd64 v0.3.3 // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/dvsekhvalnov/jose2go v1.5.0 // indirect github.com/go-faster/city v1.0.1 // indirect github.com/go-faster/errors v0.7.1 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/goccy/go-json v0.10.5 // indirect + github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/google/flatbuffers v25.12.19+incompatible // indirect github.com/gorilla/websocket v1.5.3 // indirect + github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect github.com/hashicorp/go-version v1.8.0 // indirect github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -68,6 +74,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/montanaflynn/stats v0.7.1 // indirect + github.com/mtibben/percent v0.2.1 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect github.com/paulmach/orb v0.12.0 // indirect github.com/pierrec/lz4/v4 v4.1.25 // indirect @@ -100,6 +107,7 @@ require ( golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/telemetry v0.0.0-20260116145544-c6413dc483f5 // indirect + golang.org/x/term v0.39.0 // indirect golang.org/x/tools v0.41.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect modernc.org/libc v1.67.6 // indirect diff --git a/go.sum b/go.sum index 6579c99..0bd72f3 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,10 @@ gitea.com/kingbase/gokb v0.0.0-20201021123113-29bd62a876c3 h1:QjslQNaH5Nuap5i4ni gitea.com/kingbase/gokb v0.0.0-20201021123113-29bd62a876c3/go.mod h1:7lH5A1jzCXD9Nl16DzaBUOfDAT8NPrDmZwKu1p5wf94= gitee.com/chunanyong/dm v1.8.22 h1:H7fsrnUIvEA0jlDWew7vwELry1ff+tLMIu2Fk2cIBSg= gitee.com/chunanyong/dm v1.8.22/go.mod h1:EPRJnuPFgbyOFgJ0TRYCTGzhq+ZT4wdyaj/GW/LLcNg= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= +github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0= +github.com/99designs/keyring v1.2.2/go.mod h1:wes/FrByc8j7lFOAGLGSNEg8f/PaI3cgTBqhFkHUrPk= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4= @@ -34,6 +38,8 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= +github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -56,6 +62,8 @@ github.com/duckdb/duckdb-go/v2 v2.5.5 h1:TlK8ipnzoKW2aNrjGqRkFWLCDpJDxR/VwH8ezEc github.com/duckdb/duckdb-go/v2 v2.5.5/go.mod h1:6uIbC3gz36NCEygECzboygOo/Z9TeVwox/puG+ohWV0= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/dvsekhvalnov/jose2go v1.5.0 h1:3j8ya4Z4kMCwT5nXIKFSV84YS+HdqSSO0VsTQxaLAeM= +github.com/dvsekhvalnov/jose2go v1.5.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw= github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw= github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg= @@ -68,6 +76,8 @@ github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPE github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -95,6 +105,8 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= @@ -158,6 +170,8 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= +github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= @@ -201,6 +215,7 @@ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= @@ -300,6 +315,7 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -342,6 +358,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/secretstore/keyring_store.go b/internal/secretstore/keyring_store.go new file mode 100644 index 0000000..93fe0bc --- /dev/null +++ b/internal/secretstore/keyring_store.go @@ -0,0 +1,104 @@ +package secretstore + +import ( + "errors" + "fmt" + "runtime" + + "github.com/99designs/keyring" +) + +type keyringClient interface { + Get(key string) (keyring.Item, error) + Set(item keyring.Item) error + Remove(key string) error +} + +type keyringStore struct { + ring keyringClient +} + +type keyringOpener func(cfg keyring.Config) (keyring.Keyring, error) + +func NewKeyringStore() SecretStore { + return newKeyringStoreWithOpener(runtime.GOOS, keyring.Open) +} + +func newKeyringStoreWithOpener(goos string, open keyringOpener) SecretStore { + cfg, err := keyringConfigFor(goos) + if err != nil { + return NewUnavailableStore(err.Error()) + } + + ring, err := open(cfg) + if err != nil { + return NewUnavailableStore(err.Error()) + } + + return &keyringStore{ring: ring} +} + +func (s *keyringStore) Put(ref string, payload []byte) error { + return wrapKeyringError(s.ring.Set(keyring.Item{Key: ref, Data: payload})) +} + +func (s *keyringStore) Get(ref string) ([]byte, error) { + item, err := s.ring.Get(ref) + if err != nil { + return nil, wrapKeyringError(err) + } + return item.Data, nil +} + +func (s *keyringStore) Delete(ref string) error { + return wrapKeyringError(s.ring.Remove(ref)) +} + +func (s *keyringStore) HealthCheck() error { + _, err := s.ring.Get(healthCheckRef) + if err == nil || errors.Is(err, keyring.ErrKeyNotFound) { + return nil + } + return wrapKeyringError(err) +} + +func wrapKeyringError(err error) error { + if err == nil || errors.Is(err, keyring.ErrKeyNotFound) || IsUnavailable(err) { + return err + } + return &UnavailableError{Reason: err.Error()} +} + +func keyringConfigFor(goos string) (keyring.Config, error) { + backends := allowedBackendsFor(goos) + if len(backends) == 0 { + return keyring.Config{}, fmt.Errorf("unsupported keyring platform: %s", goos) + } + + return keyring.Config{ + ServiceName: serviceName, + AllowedBackends: backends, + KeychainTrustApplication: true, + KeychainAccessibleWhenUnlocked: true, + LibSecretCollectionName: "default", + KeyCtlScope: "user", + WinCredPrefix: serviceName, + }, nil +} + +func allowedBackendsFor(goos string) []keyring.BackendType { + switch goos { + case "windows": + return []keyring.BackendType{keyring.WinCredBackend} + case "darwin": + return []keyring.BackendType{keyring.KeychainBackend} + case "linux": + return []keyring.BackendType{ + keyring.SecretServiceBackend, + keyring.KWalletBackend, + keyring.KeyCtlBackend, + } + default: + return nil + } +} diff --git a/internal/secretstore/keyring_store_test.go b/internal/secretstore/keyring_store_test.go new file mode 100644 index 0000000..4d387f0 --- /dev/null +++ b/internal/secretstore/keyring_store_test.go @@ -0,0 +1,102 @@ +package secretstore + +import ( + "errors" + "testing" + + "github.com/99designs/keyring" +) + +func TestBuildRefRejectsEmptyKind(t *testing.T) { + t.Parallel() + + if _, err := BuildRef("", "secret-id"); err == nil { + t.Fatal("BuildRef should reject an empty kind") + } +} + +func TestBuildRefRejectsEmptyID(t *testing.T) { + t.Parallel() + + if _, err := BuildRef("database", ""); err == nil { + t.Fatal("BuildRef should reject an empty id") + } +} + +func TestUnavailableStoreHealthCheckReturnsUnavailableError(t *testing.T) { + t.Parallel() + + store := NewUnavailableStore("keyring backend disabled") + + err := store.HealthCheck() + if err == nil { + t.Fatal("HealthCheck should return an unavailable error") + } + + if !IsUnavailable(err) { + t.Fatalf("HealthCheck error should be detected by IsUnavailable, got %T", err) + } +} + +func TestKeyringStoreHealthCheckTreatsMissingProbeItemAsHealthy(t *testing.T) { + t.Parallel() + + store := &keyringStore{ring: fakeKeyringClient{getErr: keyring.ErrKeyNotFound}} + if err := store.HealthCheck(); err != nil { + t.Fatalf("HealthCheck should accept ErrKeyNotFound, got %v", err) + } +} + +func TestKeyringStoreHealthCheckReturnsUnavailableErrorOnBackendFailure(t *testing.T) { + t.Parallel() + + store := &keyringStore{ring: fakeKeyringClient{getErr: errors.New("backend offline")}} + if err := store.HealthCheck(); err == nil || !IsUnavailable(err) { + t.Fatalf("HealthCheck should wrap backend failures as unavailable, got %v", err) + } +} + +func TestNewKeyringStoreReturnsUnavailableStoreWhenOpenFails(t *testing.T) { + t.Parallel() + + store := newKeyringStoreWithOpener("windows", func(cfg keyring.Config) (keyring.Keyring, error) { + if len(cfg.AllowedBackends) != 1 || cfg.AllowedBackends[0] != keyring.WinCredBackend { + t.Fatalf("unexpected backend config: %#v", cfg.AllowedBackends) + } + return nil, errors.New("no backend") + }) + + if err := store.HealthCheck(); err == nil || !IsUnavailable(err) { + t.Fatalf("expected unavailable store when open fails, got %v", err) + } +} + +type fakeKeyringClient struct { + getErr error + item keyring.Item + removeErr error +} + +func (f fakeKeyringClient) Get(string) (keyring.Item, error) { + if f.getErr != nil { + return keyring.Item{}, f.getErr + } + return f.item, nil +} + +func (f fakeKeyringClient) Set(item keyring.Item) error { + _ = item + return nil +} + +func (f fakeKeyringClient) Remove(string) error { + return f.removeErr +} + +func (f fakeKeyringClient) GetMetadata(string) (keyring.Metadata, error) { + return keyring.Metadata{}, nil +} + +func (f fakeKeyringClient) Keys() ([]string, error) { + return nil, nil +} diff --git a/internal/secretstore/store.go b/internal/secretstore/store.go new file mode 100644 index 0000000..7716c58 --- /dev/null +++ b/internal/secretstore/store.go @@ -0,0 +1,69 @@ +package secretstore + +import ( + "errors" + "fmt" + "strings" +) + +const ( + serviceName = "gonavi" + healthCheckRef = "oskeyring://gonavi/healthcheck/ping" +) + +type SecretStore interface { + Put(ref string, payload []byte) error + Get(ref string) ([]byte, error) + Delete(ref string) error + HealthCheck() error +} + +type UnavailableError struct { + Reason string +} + +func (e *UnavailableError) Error() string { + reason := strings.TrimSpace(e.Reason) + if reason == "" { + return "secret store unavailable" + } + return fmt.Sprintf("secret store unavailable: %s", reason) +} + +func IsUnavailable(err error) bool { + var target *UnavailableError + return errors.As(err, &target) +} + +type unavailableStore struct { + err error +} + +func NewUnavailableStore(reason string) SecretStore { + return unavailableStore{err: &UnavailableError{Reason: strings.TrimSpace(reason)}} +} + +func (s unavailableStore) Put(string, []byte) error { + return s.err +} + +func (s unavailableStore) Get(string) ([]byte, error) { + return nil, s.err +} + +func (s unavailableStore) Delete(string) error { + return s.err +} + +func (s unavailableStore) HealthCheck() error { + return s.err +} + +func BuildRef(kind, id string) (string, error) { + kind = strings.TrimSpace(kind) + id = strings.TrimSpace(id) + if kind == "" || id == "" { + return "", fmt.Errorf("invalid secret ref") + } + return fmt.Sprintf("oskeyring://%s/%s/%s", serviceName, kind, id), nil +}