Skip to content
Open
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
34 changes: 34 additions & 0 deletions store/keychain/internal/go-keychain/secretservice/secretservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,40 @@ func (s *SecretService) PromptAndWait(prompt dbus.ObjectPath) (paths *dbus.Varia
}
}

// SetItemSecret replaces an existing item's secret value in place via
// org.freedesktop.Secret.Item.SetSecret. The secret must already be encoded for
// the session it references (see [Session.NewSecret]). SetSecret takes a single
// Secret argument (D-Bus signature (oayays)) and returns no values, so there is
// no prompt path: the item's collection must be unlocked first (use [Unlock]),
// otherwise the call fails.
func (s *SecretService) SetItemSecret(item dbus.ObjectPath, secret Secret) error {
if err := s.Obj(item).Call("org.freedesktop.Secret.Item.SetSecret", NilFlags, secret).Store(); err != nil {
return fmt.Errorf("failed to set item secret: %w", err)
}
return nil
}

// SetItemAttributes replaces an existing item's lookup attributes in place by
// setting the read-write org.freedesktop.Secret.Item.Attributes property
// (type a{ss}) through org.freedesktop.DBus.Properties.Set. The collection must
// be unlocked.
func (s *SecretService) SetItemAttributes(item dbus.ObjectPath, attributes Attributes) error {
if err := s.Obj(item).SetProperty("org.freedesktop.Secret.Item.Attributes", dbus.MakeVariant(map[string]string(attributes))); err != nil {
return fmt.Errorf("failed to set item attributes: %w", err)
}
return nil
}

// SetItemLabel replaces an existing item's displayable label in place by setting
// the read-write org.freedesktop.Secret.Item.Label property (type s) through
// org.freedesktop.DBus.Properties.Set. The collection must be unlocked.
func (s *SecretService) SetItemLabel(item dbus.ObjectPath, label string) error {
if err := s.Obj(item).SetProperty("org.freedesktop.Secret.Item.Label", dbus.MakeVariant(label)); err != nil {
return fmt.Errorf("failed to set item label: %w", err)
}
return nil
}

// NewSecretProperties
func NewSecretProperties(label string, attributes map[string]string) map[string]dbus.Variant {
return map[string]dbus.Variant{
Expand Down
37 changes: 35 additions & 2 deletions store/keychain/keychain_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ type secretService interface {
DeleteItem(item dbus.ObjectPath) error
GetAttributes(item dbus.ObjectPath) (kc.Attributes, error)
GetSecret(item dbus.ObjectPath, session kc.Session) ([]byte, error)
SetItemSecret(item dbus.ObjectPath, secret kc.Secret) error
SetItemAttributes(item dbus.ObjectPath, attributes kc.Attributes) error
SetItemLabel(item dbus.ObjectPath, label string) error
Close() error
}

Expand Down Expand Up @@ -385,13 +388,43 @@ func (k *keychainStore[T]) Save(_ context.Context, id store.ID, secret store.Sec
safelySetID(id, attributes)

label := k.itemLabel(id.String())
properties := kc.NewSecretProperties(label, attributes)

_, err = service.CreateItem(objectPath, properties, sessSecret, kc.ReplaceBehaviorReplace)
// Find existing items for this identity by the stable triple only
// {service:group, service:name, id}, never the volatile metadata, so a
// changed metadata value can never hide a previously-stored item. This is
// what makes the in-place update below reliable and stops the duplicate
// accumulation described in issue #446.
ident := make(map[string]string)
safelySetMetadata(k.serviceGroup, k.serviceName, ident)
safelySetID(id, ident)

items, err := service.SearchCollection(objectPath, ident)
if err != nil {
return err
}

// Nothing stored yet: create a fresh item.
if len(items) == 0 {
properties := kc.NewSecretProperties(label, attributes)
_, err = service.CreateItem(objectPath, properties, sessSecret, kc.ReplaceBehaviorReplace)
return err
}

// Update the first match in place. Its object path is preserved, so the
// secret is never momentarily absent and no duplicate is minted. Writing the
// secret value IS the operation, so only its failure fails Save; refreshing
// the attributes and label and collapsing any pre-existing duplicates are
// best-effort (the secret is already stored) and must not flip the result.
primary := items[0]
if err := service.SetItemSecret(primary, sessSecret); err != nil {
return err
}
_ = service.SetItemAttributes(primary, attributes)
_ = service.SetItemLabel(primary, label)
for _, dup := range items[1:] {
_ = service.DeleteItem(dup)
}
Comment on lines +418 to +426

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

migration step for secrets that are currently being duplicated. future writes won't duplicate


return nil
}

Expand Down
Loading
Loading