Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions cmd/cloudstic/cmd_store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"errors"
"os"
"path/filepath"
"runtime"
"strings"
"testing"

Expand Down Expand Up @@ -1181,7 +1180,7 @@ func TestConfigureStoreEncryptionSelection_KMSError(t *testing.T) {

func TestPromptSecretReference_EnvInteractive(t *testing.T) {
t.Setenv("MY_ENV", "set-for-test")
if runtime.GOOS == "darwin" {
if len(profileSecretResolver.WritableBackends()) > 0 {
setInteractiveStdinLines(t, "1", "MY_ENV")
} else {
setInteractiveStdinLines(t, "MY_ENV")
Expand Down Expand Up @@ -1210,7 +1209,7 @@ func TestPromptEncryptionConfig_PasswordViaEnvRef(t *testing.T) {
},
}

if runtime.GOOS == "darwin" {
if len(profileSecretResolver.WritableBackends()) > 0 {
setInteractiveStdinLines(t, "1", "1", "MY_BACKUP_PASSWORD")
} else {
setInteractiveStdinLines(t, "1", "MY_BACKUP_PASSWORD")
Expand Down
59 changes: 56 additions & 3 deletions internal/secretref/secret_service_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,41 @@ var (
)

type secretServiceLookupFunc func(ctx context.Context, collection, item string) (string, error)
type secretServiceExistsFunc func(ctx context.Context, collection, item string) (bool, error)
type secretServiceStoreFunc func(ctx context.Context, collection, item, value string) error

// SecretServiceBackend resolves secret-service://collection/item references.
type SecretServiceBackend struct {
lookup secretServiceLookupFunc
exists secretServiceExistsFunc
store secretServiceStoreFunc
}

// NewSecretServiceBackend creates a Secret Service backend for the current
// platform.
func NewSecretServiceBackend() *SecretServiceBackend {
return &SecretServiceBackend{lookup: defaultSecretServiceLookup}
return &SecretServiceBackend{
lookup: defaultSecretServiceLookup,
exists: defaultSecretServiceExists,
store: defaultSecretServiceStore,
}
}

func newSecretServiceBackendWithLookup(lookup secretServiceLookupFunc) *SecretServiceBackend {
func newSecretServiceBackendWithFns(lookup secretServiceLookupFunc, exists secretServiceExistsFunc, store secretServiceStoreFunc) *SecretServiceBackend {
if lookup == nil {
lookup = defaultSecretServiceLookup
}
return &SecretServiceBackend{lookup: lookup}
if exists == nil {
exists = defaultSecretServiceExists
}
if store == nil {
store = defaultSecretServiceStore
}
return &SecretServiceBackend{lookup: lookup, exists: exists, store: store}
}

func newSecretServiceBackendWithLookup(lookup secretServiceLookupFunc) *SecretServiceBackend {
return newSecretServiceBackendWithFns(lookup, nil, nil)
}

func parseSecretServicePath(path string) (collection string, item string, err error) {
Expand Down Expand Up @@ -65,3 +83,38 @@ func (b *SecretServiceBackend) Resolve(ctx context.Context, ref Ref) (string, er

return value, nil
}

func (b *SecretServiceBackend) Scheme() string { return "secret-service" }

func (b *SecretServiceBackend) DisplayName() string { return "Secret Service" }

func (b *SecretServiceBackend) WriteSupported() bool { return defaultSecretServiceWriteSupported() }

func (b *SecretServiceBackend) DefaultRef(storeName, account string) string {
return "secret-service://cloudstic/" + storeName + "/" + account
}

func (b *SecretServiceBackend) Exists(ctx context.Context, ref Ref) (bool, error) {
collection, item, err := parseSecretServicePath(ref.Path)
if err != nil {
return false, errorf(KindInvalidRef, ref.Raw, err.Error(), nil)
}

exists, err := b.exists(ctx, collection, item)
if err != nil {
return false, errorf(KindBackendUnavailable, ref.Raw, err.Error(), err)
}
return exists, nil
}

func (b *SecretServiceBackend) Store(ctx context.Context, ref Ref, value string) error {
collection, item, err := parseSecretServicePath(ref.Path)
if err != nil {
return errorf(KindInvalidRef, ref.Raw, err.Error(), nil)
}

if err := b.store(ctx, collection, item, value); err != nil {
return errorf(KindBackendUnavailable, ref.Raw, err.Error(), err)
}
return nil
}
55 changes: 55 additions & 0 deletions internal/secretref/secret_service_backend_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ var secretServiceSessionBus = func() (secretServiceDBusConn, error) {
return dbus.SessionBus()
}

func defaultSecretServiceWriteSupported() bool { return true }

func defaultSecretServiceLookup(_ context.Context, collection, item string) (string, error) {
conn, err := secretServiceSessionBus()
if err != nil {
Expand Down Expand Up @@ -64,6 +66,59 @@ func defaultSecretServiceLookup(_ context.Context, collection, item string) (str
return string(secret.Value), nil
}

func defaultSecretServiceExists(ctx context.Context, collection, item string) (bool, error) {
_, err := defaultSecretServiceLookup(ctx, collection, item)
if err != nil {
switch err {
case errSecretServiceNotFound:
return false, nil
default:
return false, err
}
}
return true, nil
}

func defaultSecretServiceStore(_ context.Context, collection, item, value string) error {
conn, err := secretServiceSessionBus()
if err != nil {
return fmt.Errorf("%w: cannot connect to the session bus; ensure a desktop keyring/DBus session is available or use env://... as a fallback", errSecretServiceUnavailable)
}
defer func() { _ = conn.Close() }()

service := conn.Object(secretServiceName, secretServicePath)
collectionPath, err := lookupSecretServiceCollection(conn, service, collection)
if err != nil {
return err
}

var ignored dbus.Variant
var session dbus.ObjectPath
if err := service.Call(secretServiceInterface+".OpenSession", 0, "plain", dbus.MakeVariant("")).Store(&ignored, &session); err != nil {
return mapSecretServiceCallError(err, "open Secret Service session")
}

properties := map[string]dbus.Variant{
itemInterface + ".Label": dbus.MakeVariant(item),
itemInterface + ".Attributes": dbus.MakeVariant(map[string]string{"cloudstic_ref": collection + "/" + item}),
}
secret := secretServiceSecret{
Session: session,
Parameters: nil,
Value: []byte(value),
ContentType: "text/plain; charset=utf-8",
}
var itemPath dbus.ObjectPath
var prompt dbus.ObjectPath
if err := conn.Object(secretServiceName, collectionPath).Call(collectionInterface+".CreateItem", 0, properties, secret, true).Store(&itemPath, &prompt); err != nil {
return mapSecretServiceCallError(err, "write secret to Secret Service")
}
if prompt != "" && prompt != "/" {
return fmt.Errorf("%w: Secret Service write requires user interaction in this session; use env://... as a fallback", errSecretServiceUnavailable)
}
return nil
}

func lookupSecretServiceCollection(conn secretServiceDBusConn, service dbus.BusObject, want string) (dbus.ObjectPath, error) {
if aliasPath, err := readSecretServiceAlias(service, want); err == nil && aliasPath != "" && aliasPath != "/" {
return aliasPath, nil
Expand Down
14 changes: 14 additions & 0 deletions internal/secretref/secret_service_backend_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,17 @@ func TestDefaultSecretServiceLookupMissingSessionBus(t *testing.T) {
t.Fatalf("expected errSecretServiceUnavailable, got %v", err)
}
}

func TestDefaultSecretServiceExistsNotFound(t *testing.T) {
orig := secretServiceSessionBus
defer func() { secretServiceSessionBus = orig }()

secretServiceSessionBus = func() (secretServiceDBusConn, error) {
return nil, errors.New("dbus session unavailable")
}

_, err := defaultSecretServiceExists(context.Background(), "cloudstic", "prod/password")
if !errors.Is(err, errSecretServiceUnavailable) {
t.Fatalf("expected errSecretServiceUnavailable, got %v", err)
}
}
10 changes: 10 additions & 0 deletions internal/secretref/secret_service_backend_stub.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ import (
"fmt"
)

func defaultSecretServiceWriteSupported() bool { return false }

func defaultSecretServiceLookup(_ context.Context, _, _ string) (string, error) {
return "", fmt.Errorf("%w: secret service backend is only available on Linux; use env://... as a fallback in headless environments", errSecretServiceUnavailable)
}

func defaultSecretServiceExists(_ context.Context, _, _ string) (bool, error) {
return false, fmt.Errorf("%w: secret service backend is only available on Linux; use env://... as a fallback in headless environments", errSecretServiceUnavailable)
}

func defaultSecretServiceStore(_ context.Context, _, _, _ string) error {
return fmt.Errorf("%w: secret service backend is only available on Linux; use env://... as a fallback in headless environments", errSecretServiceUnavailable)
}
43 changes: 43 additions & 0 deletions internal/secretref/secret_service_backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,46 @@ func TestSecretServiceBackendResolveErrors(t *testing.T) {
})
}
}

func TestSecretServiceBackend_DefaultRef(t *testing.T) {
b := NewSecretServiceBackend()
if got := b.DefaultRef("prod", "password"); got != "secret-service://cloudstic/prod/password" {
t.Fatalf("DefaultRef() = %q", got)
}
}

func TestSecretServiceBackend_Exists(t *testing.T) {
b := newSecretServiceBackendWithFns(
func(context.Context, string, string) (string, error) { return "", nil },
func(_ context.Context, collection, item string) (bool, error) {
if collection != "cloudstic" || item != "prod/password" {
t.Fatalf("unexpected args %q/%q", collection, item)
}
return true, nil
},
nil,
)
exists, err := b.Exists(context.Background(), Ref{Raw: "secret-service://cloudstic/prod/password", Scheme: "secret-service", Path: "cloudstic/prod/password"})
if err != nil {
t.Fatalf("Exists: %v", err)
}
if !exists {
t.Fatal("expected exists=true")
}
}

func TestSecretServiceBackend_Store(t *testing.T) {
b := newSecretServiceBackendWithFns(
func(context.Context, string, string) (string, error) { return "", nil },
nil,
func(_ context.Context, collection, item, value string) error {
if collection != "cloudstic" || item != "prod/password" || value != "secret" {
t.Fatalf("unexpected args %q/%q/%q", collection, item, value)
}
return nil
},
)
if err := b.Store(context.Background(), Ref{Raw: "secret-service://cloudstic/prod/password", Scheme: "secret-service", Path: "cloudstic/prod/password"}, "secret"); err != nil {
t.Fatalf("Store: %v", err)
}
}
59 changes: 56 additions & 3 deletions internal/secretref/wincred_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,41 @@ var (
)

type wincredLookupFunc func(ctx context.Context, target string) (string, error)
type wincredExistsFunc func(ctx context.Context, target string) (bool, error)
type wincredStoreFunc func(ctx context.Context, target, value string) error

// WincredBackend resolves wincred://target references.
type WincredBackend struct {
lookup wincredLookupFunc
exists wincredExistsFunc
store wincredStoreFunc
}

// NewWincredBackend creates a Windows Credential Manager backend for the
// current platform.
func NewWincredBackend() *WincredBackend {
return &WincredBackend{lookup: defaultWincredLookup}
return &WincredBackend{
lookup: defaultWincredLookup,
exists: defaultWincredExists,
store: defaultWincredStore,
}
}

func newWincredBackendWithLookup(lookup wincredLookupFunc) *WincredBackend {
func newWincredBackendWithFns(lookup wincredLookupFunc, exists wincredExistsFunc, store wincredStoreFunc) *WincredBackend {
if lookup == nil {
lookup = defaultWincredLookup
}
return &WincredBackend{lookup: lookup}
if exists == nil {
exists = defaultWincredExists
}
if store == nil {
store = defaultWincredStore
}
return &WincredBackend{lookup: lookup, exists: exists, store: store}
}

func newWincredBackendWithLookup(lookup wincredLookupFunc) *WincredBackend {
return newWincredBackendWithFns(lookup, nil, nil)
}

func parseWincredTarget(path string) (string, error) {
Expand Down Expand Up @@ -61,3 +79,38 @@ func (b *WincredBackend) Resolve(ctx context.Context, ref Ref) (string, error) {

return value, nil
}

func (b *WincredBackend) Scheme() string { return "wincred" }

func (b *WincredBackend) DisplayName() string { return "Windows Credential Manager" }

func (b *WincredBackend) WriteSupported() bool { return defaultWincredWriteSupported() }

func (b *WincredBackend) DefaultRef(storeName, account string) string {
return "wincred://cloudstic/store/" + storeName + "/" + account
}

func (b *WincredBackend) Exists(ctx context.Context, ref Ref) (bool, error) {
target, err := parseWincredTarget(ref.Path)
if err != nil {
return false, errorf(KindInvalidRef, ref.Raw, err.Error(), nil)
}

exists, err := b.exists(ctx, target)
if err != nil {
return false, errorf(KindBackendUnavailable, ref.Raw, err.Error(), err)
}
return exists, nil
}

func (b *WincredBackend) Store(ctx context.Context, ref Ref, value string) error {
target, err := parseWincredTarget(ref.Path)
if err != nil {
return errorf(KindInvalidRef, ref.Raw, err.Error(), nil)
}

if err := b.store(ctx, target, value); err != nil {
return errorf(KindBackendUnavailable, ref.Raw, err.Error(), err)
}
return nil
}
Loading
Loading