secretservice

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 13, 2026 License: MIT Imports: 25 Imported by: 0

Documentation

Overview

Package secretservice implements the org.freedesktop.secrets provider on top of the password store, so that programs which already speak Secret Service — Chrome, VS Code, NetworkManager, Evolution — read their secrets from the same store binpass manages, without knowing it exists.

Why the attributes are not stored in the clear

A Secret Service item is a password plus a set of attributes: application, server, username, and so on. pass-secret-service writes those attributes in plaintext beside the ciphertext, which its README states outright. That means the full map of every account — every host you have an account with and under what name — sits unencrypted in whatever git host or cloud drive the store is synchronised to, even though the passwords themselves are encrypted.

This implementation puts the attributes inside the encrypted file and keeps a separate index of keyed hashes for lookup. The specification is what makes that possible: SearchItems matches attribute pairs exactly, never by substring or prefix, so a deterministic HMAC is a complete search index rather than a compromise. What leaks is that two items share some attribute value, which is a great deal less than naming the values.

Index

Constants

View Source
const (
	// ServicePath is the root object.
	ServicePath = dbus.ObjectPath("/org/freedesktop/secrets")
	// ServiceInterface is the interface the root object implements.
	ServiceInterface = "org.freedesktop.Secret.Service"
	// CollectionInterface is implemented by each collection.
	CollectionInterface = "org.freedesktop.Secret.Collection"
	// ItemInterface is implemented by each item.
	ItemInterface = "org.freedesktop.Secret.Item"
	// SessionInterface is implemented by each session.
	SessionInterface = "org.freedesktop.Secret.Session"
	// PromptInterface is implemented by prompts.
	PromptInterface = "org.freedesktop.Secret.Prompt"
)

The bus name and object paths the specification fixes. Clients hardcode these, so none of them is configurable.

View Source
const (
	// AlgPlain sends secrets unencrypted over the bus. The bus socket is
	// already restricted to this user, so this is what most clients use.
	AlgPlain = "plain"
	// AlgDH negotiates a shared key by Diffie-Hellman and encrypts the
	// secret with it. libsecret asks for this by default, so a provider
	// without it does not work with the client library most programs use.
	AlgDH = "dh-ietf1024-sha256-aes128-cbc-pkcs7"
)

The transport algorithms named by the specification.

View Source
const BusName = "org.freedesktop.secrets"

BusName is the well-known name a Secret Service provider owns. Only one process on a session bus can hold it, which is why `ss doctor` exists.

Declared here rather than beside the D-Bus implementation because the CLI names it in messages on every platform, including the ones where the provider cannot run.

View Source
const DefaultCollection = "login"

DefaultCollection is the collection clients get when they ask for the default alias. "login" is what gnome-keyring calls its own, and programs that hardcode a collection name overwhelmingly hardcode that one.

View Source
const IndexVersion = 1

IndexVersion is the current on-disk index format.

View Source
const Root = "secret-service"

Root is the subtree of the password store that holds Secret Service items.

Keeping them under one directory means a user can see exactly what programs have stored, back it up, and delete the lot, without picking items out of their own entries.

Variables

View Source
var ErrLocked = errors.New("secretservice: store is locked")

ErrLocked reports an operation needing a store that cannot be decrypted.

View Source
var ErrNameTaken = errors.New("secretservice: " + BusName + " is already owned")

ErrNameTaken reports that another provider owns the bus name.

View Source
var ErrNotFound = errors.New("secretservice: not found")

ErrNotFound reports an item or collection that does not exist.

View Source
var ErrUnsupportedAlgorithm = errors.New("secretservice: unsupported algorithm")

ErrUnsupportedAlgorithm reports a transport algorithm this provider does not implement.

Functions

func Blind

func Blind(indexKey []byte, name, value string) string

Blind computes the lookup key for one attribute pair.

The name and value are separated by a NUL so that the pairs ("ab", "c") and ("a", "bc") cannot produce the same key — without a separator they would hash the same bytes, and an attacker who could choose attribute names would decide what a search matches.

The index key never leaves the machine: it is derived from the store's identity, so a provider holding the index sees base32 of a keyed hash and cannot compute the hash of a guess.

func Owner

func Owner() (string, bool, error)

Owner returns who currently owns the Secret Service bus name.

This is what `binpass ss doctor` reports: the usual reason the provider will not start is that gnome-keyring is already there, and naming it turns a refusal into an instruction.

func Serve

func Serve(ctx context.Context, opts ServeOptions) error

Serve runs the provider until the context is cancelled.

Types

type AccessRecord

type AccessRecord struct {
	// When the access happened.
	When time.Time
	// Caller is the executable that asked, when it could be identified.
	Caller string
	// Sender is the caller's unique bus name.
	Sender string
	// Collection holds the item.
	Collection string
	// Item is the item's identifier.
	Item string
	// Label is the item's label, which is easier to recognise than its ID.
	Label string
	// Allowed is what was decided.
	Allowed bool
}

AccessRecord is one line of the audit log.

type Action

type Action string

Action is what a policy rule decides.

const (
	// ActionAllow hands over the secret.
	ActionAllow Action = "allow"
	// ActionDeny refuses it.
	ActionDeny Action = "deny"
	// ActionPrompt asks the user.
	ActionPrompt Action = "prompt"
)

The decisions a rule can make.

type Config

type Config struct {
	// Default applies when no rule matches.
	Default Action `yaml:"default"`
	// Remember is how long a prompt decision is honoured.
	Remember time.Duration `yaml:"remember"`
	// Rules are evaluated in order; the first match wins.
	Rules []Rule `yaml:"rules"`
	// Notify controls access notifications: off, on-access, on-prompt.
	Notify string `yaml:"notify"`
	// Audit records every access to the log.
	Audit bool `yaml:"audit"`
}

Config is the access policy.

func DefaultConfig

func DefaultConfig() Config

DefaultConfig returns the policy used when none is configured.

The default is allow, matching what gnome-keyring and pass-secret-service do. Denying by default would break every program on the machine the moment the provider starts, and a security control that has to be turned off to get work done is one that gets turned off.

type Index

type Index struct {
	// Version allows the on-disk format to change without silently
	// misreading an older file.
	Version int `json:"version"`
	// Entries maps a blinded pair to the item IDs that carry it.
	Entries map[string][]string `json:"entries"`
}

Index maps blinded attribute pairs to the items carrying them.

It exists so that a search costs one map lookup rather than decrypting every item in the store: with a few hundred items and a hardware token, the difference is between instant and unusable.

func NewIndex

func NewIndex() *Index

NewIndex returns an empty index.

func (*Index) Add

func (ix *Index) Add(indexKey []byte, it *Item)

Add records every attribute of an item.

func (*Index) MarshalJSON

func (ix *Index) MarshalJSON() ([]byte, error)

MarshalJSON is the on-disk form. The index is written encrypted, so this is only ever the plaintext inside that file.

func (*Index) Remove

func (ix *Index) Remove(id string)

Remove drops an item from every pair it was recorded under.

func (*Index) Search

func (ix *Index) Search(indexKey []byte, query map[string]string) []string

Search returns the IDs of items carrying every pair in the query.

An empty query returns nothing rather than everything: callers that want the whole collection ask the store for it, and having the two cases look alike here would make "match nothing" and "match all" one typo apart.

func (*Index) UnmarshalJSON

func (ix *Index) UnmarshalJSON(data []byte) error

UnmarshalJSON reads the on-disk form, refusing a version it was not written to understand.

type Item

type Item struct {
	// ID is the item's identifier, unique within its collection, and the
	// last element of its D-Bus object path.
	ID string
	// Label is the human-readable name shown by keyring browsers.
	Label string
	// Attributes are the searchable key/value pairs.
	Attributes map[string]string
	// Secret is the secret value itself.
	Secret string
	// Created is when the item was first stored.
	Created time.Time
	// Modified is when the secret or its attributes last changed.
	Modified time.Time
}

Item is one Secret Service item: a secret plus the attributes programs search it by.

func ItemFromSecret

func ItemFromSecret(id string, sec *secret.Secret) *Item

ItemFromSecret parses a stored entry back into an item.

Anything that is not a recognised field is left alone rather than discarded: a user who adds a note to an item's file should still have it there after a program updates the secret.

func (*Item) Matches

func (i *Item) Matches(query map[string]string) bool

Matches reports whether the item carries every attribute in query with the given value.

The specification calls for exact matching on each pair, and an empty query matches everything: that is how a client asks for the contents of a collection.

func (*Item) ToSecret

func (i *Item) ToSecret() *secret.Secret

ToSecret renders an item as a pass-format secret.

The layout is deliberately one a human can read with `binpass show` and `pass show`: the secret on the first line, then ordinary `key: value` fields. An item written by a browser stays an entry you can inspect and edit by hand.

type PassStore

type PassStore interface {
	// Get decrypts the entry called name.
	Get(name string) (*secret.Secret, error)
	// Set encrypts a secret to the entry called name.
	Set(name string, sec *secret.Secret) error
	// List returns the entry names under sub.
	List(sub string) ([]string, error)
	// Remove deletes the entry called name.
	Remove(name string) error
	// Exists reports whether an entry exists.
	Exists(name string) bool
}

PassStore is the part of pkg/store this package needs. Depending on an interface keeps the D-Bus layer testable without a real store on disk.

type Policy

type Policy interface {
	// Allow reports whether the caller identified by the bus sender may
	// read the item, and returns the reason when it may not.
	Allow(sender string, collection string, it *Item) (bool, string)
	// Record notes an access for the audit log.
	Record(sender string, collection string, it *Item, allowed bool)
}

Policy decides whether a caller may have a secret. It is consulted before anything is decrypted, so a denial means the plaintext is never produced.

type PolicyEngine

type PolicyEngine struct {
	// contains filtered or unexported fields
}

PolicyEngine decides access and keeps the audit log.

func NewPolicyEngine

func NewPolicyEngine(cfg Config, identify func(sender string) (string, error), notify io.Writer) *PolicyEngine

NewPolicyEngine returns an engine for a configuration.

identify may be nil, in which case rules matching on the caller's executable never match and the default applies.

func (*PolicyEngine) Allow

func (p *PolicyEngine) Allow(sender, collection string, it *Item) (bool, string)

Allow implements Policy.

func (*PolicyEngine) LastAccessor

func (p *PolicyEngine) LastAccessor(id string) (AccessRecord, bool)

LastAccessor returns the most recent access to an item.

func (*PolicyEngine) Log

func (p *PolicyEngine) Log() []AccessRecord

Log returns the recorded accesses, newest last.

func (*PolicyEngine) Record

func (p *PolicyEngine) Record(sender, collection string, it *Item, allowed bool)

Record implements Policy.

func (*PolicyEngine) Remember

func (p *PolicyEngine) Remember(caller, collection, itemID string, allowed bool)

Remember records a prompt answer for the configured duration.

type Rule

type Rule struct {
	// App matches the caller's executable path. "*" matches anything.
	App string `yaml:"app,omitempty"`
	// Collection restricts the rule to one collection.
	Collection string `yaml:"collection,omitempty"`
	// Attrs restricts the rule to items carrying these attributes.
	Attrs map[string]string `yaml:"attrs,omitempty"`
	// Action is what to do when the rule matches.
	Action Action `yaml:"action"`
}

Rule is one entry in the access policy.

type Secret

type Secret struct {
	// Session is the session the secret is encrypted for.
	Session dbus.ObjectPath
	// Parameters carries the IV for the encrypted algorithms.
	Parameters []byte
	// Value is the secret itself, encrypted unless the session is plain.
	Value []byte
	// ContentType describes the value; clients rarely set anything else.
	ContentType string
}

Secret is the wire form of a secret: the quadruple the specification passes between client and provider.

type ServeOptions

type ServeOptions struct {
	// Store is the password store items live in.
	Store PassStore
	// IndexSeed derives the attribute index key.
	IndexSeed []byte
	// Policy is the access policy.
	Policy Config
	// Takeover says what to do when the bus name is taken.
	Takeover Takeover
	// Notify receives access notifications.
	Notify *os.File
}

ServeOptions configures the provider.

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service is the D-Bus provider. It owns the bus name and exports every object a client walks: the service itself, the collections, their items, and one session per connected client.

func NewService

func NewService(conn *dbus.Conn, store *Store, policy Policy) *Service

NewService returns a provider over a store.

func (*Service) CreateCollection

func (s *Service) CreateCollection(properties map[string]dbus.Variant, alias string) (dbus.ObjectPath, dbus.ObjectPath, *dbus.Error)

CreateCollection implements org.freedesktop.Secret.Service.CreateCollection.

func (*Service) Export

func (s *Service) Export(mode Takeover) error

Export publishes every object and claims the bus name.

func (*Service) GetSecrets

func (s *Service) GetSecrets(items []dbus.ObjectPath, session dbus.ObjectPath) (map[dbus.ObjectPath]Secret, *dbus.Error)

GetSecrets implements org.freedesktop.Secret.Service.GetSecrets.

func (*Service) Lock

func (s *Service) Lock(objects []dbus.ObjectPath) ([]dbus.ObjectPath, dbus.ObjectPath, *dbus.Error)

Lock implements org.freedesktop.Secret.Service.Lock.

func (*Service) OpenSession

func (s *Service) OpenSession(algorithm string, input dbus.Variant) (dbus.Variant, dbus.ObjectPath, *dbus.Error)

OpenSession implements org.freedesktop.Secret.Service.OpenSession.

func (*Service) ReadAlias

func (s *Service) ReadAlias(name string) (dbus.ObjectPath, *dbus.Error)

ReadAlias implements org.freedesktop.Secret.Service.ReadAlias.

func (*Service) SearchItems

func (s *Service) SearchItems(attributes map[string]string) ([]dbus.ObjectPath, []dbus.ObjectPath, *dbus.Error)

SearchItems implements org.freedesktop.Secret.Service.SearchItems.

func (*Service) SetAlias

func (s *Service) SetAlias(string, dbus.ObjectPath) *dbus.Error

SetAlias implements org.freedesktop.Secret.Service.SetAlias.

func (*Service) Unlock

func (s *Service) Unlock(objects []dbus.ObjectPath) ([]dbus.ObjectPath, dbus.ObjectPath, *dbus.Error)

Unlock implements org.freedesktop.Secret.Service.Unlock.

Everything is already unlocked when the provider is running, so this reports success without a prompt rather than handing back a prompt object no client would know what to do with.

type Session

type Session struct {
	// ID identifies the session; it is the last element of its object path.
	ID string
	// Algorithm is the negotiated transport algorithm.
	Algorithm string
	// contains filtered or unexported fields
}

Session is one client's transport context.

A client opens a session, uses it for as long as it likes, and closes it. The negotiated key lives here and nowhere else, so closing a session is what makes the key unrecoverable.

func NewDHSession

func NewDHSession(id string, clientPublic []byte) (*Session, []byte, error)

NewDHSession completes a Diffie-Hellman exchange with a client's public key, returning the session and the public key to send back.

func NewPlainSession

func NewPlainSession(id string) *Session

NewPlainSession returns a session that sends secrets unencrypted.

func (*Session) Decrypt

func (s *Session) Decrypt(value, parameter []byte) ([]byte, error)

Decrypt recovers a secret a client sent.

func (*Session) Encrypt

func (s *Session) Encrypt(plaintext []byte) (value, parameter []byte, err error)

Encrypt prepares a secret for transport, returning the value and the parameter the specification carries the IV in.

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store is the persistence layer: items are entries in the password store, and the attribute index is one encrypted file beside them.

func NewStore

func NewStore(pass PassStore, seed []byte) *Store

NewStore returns a Store over a password store.

The index key is derived from a caller-supplied seed rather than generated here, so that the same store produces the same blinded keys on every machine it is synchronised to. A key that differed per machine would make each one rebuild the whole index on first use and, worse, make an index synchronised from elsewhere silently match nothing.

func (*Store) Collections

func (s *Store) Collections() ([]string, error)

Collections returns the collection names that exist.

func (*Store) CreateItem

func (s *Store) CreateItem(collection string, it *Item, replace bool) (*Item, error)

CreateItem stores an item, assigning an ID when it has none, and updates the index.

When replace is false and an item with the same attributes already exists, the existing one is returned untouched: the specification says a client asking to create a duplicate gets the original rather than a second copy.

func (*Store) DeleteItem

func (s *Store) DeleteItem(collection, id string) error

DeleteItem removes an item and its index entries.

func (*Store) Item

func (s *Store) Item(collection, id string) (*Item, error)

Item reads one item.

func (*Store) Items

func (s *Store) Items(collection string) ([]*Item, error)

Items returns every item in a collection.

func (*Store) Reindex

func (s *Store) Reindex() (int, error)

Reindex rebuilds the index from the items on disk.

Needed after a sync brings in items from another machine, and as the repair for an index that has drifted from the store for any other reason.

func (*Store) Search

func (s *Store) Search(collection string, query map[string]string) ([]*Item, error)

Search returns the items in a collection matching every attribute pair.

The index answers the query; the items it names are then read and checked again. That second pass is not redundant: the index maps a hash, and two different pairs could in principle hash alike. Confirming against the decrypted attributes means a collision returns fewer results, never wrong ones.

type Takeover

type Takeover string

Takeover says what to do when the bus name is already owned.

const (
	// TakeoverRefuse fails rather than displacing another provider.
	TakeoverRefuse Takeover = "refuse"
	// TakeoverReplace takes the name, asking the current owner to yield.
	TakeoverReplace Takeover = "replace"
	// TakeoverWait queues for the name and starts when it is free.
	TakeoverWait Takeover = "wait"
)

The takeover modes.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL