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
- Variables
- func Blind(indexKey []byte, name, value string) string
- func Owner() (string, bool, error)
- func Serve(ctx context.Context, opts ServeOptions) error
- type AccessRecord
- type Action
- type Config
- type Index
- type Item
- type PassStore
- type Policy
- type PolicyEngine
- func (p *PolicyEngine) Allow(sender, collection string, it *Item) (bool, string)
- func (p *PolicyEngine) LastAccessor(id string) (AccessRecord, bool)
- func (p *PolicyEngine) Log() []AccessRecord
- func (p *PolicyEngine) Record(sender, collection string, it *Item, allowed bool)
- func (p *PolicyEngine) Remember(caller, collection, itemID string, allowed bool)
- type Rule
- type Secret
- type ServeOptions
- type Service
- func (s *Service) CreateCollection(properties map[string]dbus.Variant, alias string) (dbus.ObjectPath, dbus.ObjectPath, *dbus.Error)
- func (s *Service) Export(mode Takeover) error
- func (s *Service) GetSecrets(items []dbus.ObjectPath, session dbus.ObjectPath) (map[dbus.ObjectPath]Secret, *dbus.Error)
- func (s *Service) Lock(objects []dbus.ObjectPath) ([]dbus.ObjectPath, dbus.ObjectPath, *dbus.Error)
- func (s *Service) OpenSession(algorithm string, input dbus.Variant) (dbus.Variant, dbus.ObjectPath, *dbus.Error)
- func (s *Service) ReadAlias(name string) (dbus.ObjectPath, *dbus.Error)
- func (s *Service) SearchItems(attributes map[string]string) ([]dbus.ObjectPath, []dbus.ObjectPath, *dbus.Error)
- func (s *Service) SetAlias(string, dbus.ObjectPath) *dbus.Error
- func (s *Service) Unlock(objects []dbus.ObjectPath) ([]dbus.ObjectPath, dbus.ObjectPath, *dbus.Error)
- type Session
- type Store
- func (s *Store) Collections() ([]string, error)
- func (s *Store) CreateItem(collection string, it *Item, replace bool) (*Item, error)
- func (s *Store) DeleteItem(collection, id string) error
- func (s *Store) Item(collection, id string) (*Item, error)
- func (s *Store) Items(collection string) ([]*Item, error)
- func (s *Store) Reindex() (int, error)
- func (s *Store) Search(collection string, query map[string]string) ([]*Item, error)
- type Takeover
Constants ¶
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.
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.
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.
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.
const IndexVersion = 1
IndexVersion is the current on-disk index format.
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 ¶
var ErrLocked = errors.New("secretservice: store is locked")
ErrLocked reports an operation needing a store that cannot be decrypted.
var ErrNameTaken = errors.New("secretservice: " + BusName + " is already owned")
ErrNameTaken reports that another provider owns the bus name.
var ErrNotFound = errors.New("secretservice: not found")
ErrNotFound reports an item or collection that does not exist.
var ErrUnsupportedAlgorithm = errors.New("secretservice: unsupported algorithm")
ErrUnsupportedAlgorithm reports a transport algorithm this provider does not implement.
Functions ¶
func Blind ¶
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.
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 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 (*Index) MarshalJSON ¶
MarshalJSON is the on-disk form. The index is written encrypted, so this is only ever the plaintext inside that file.
func (*Index) Search ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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) 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) SearchItems ¶
func (s *Service) SearchItems(attributes map[string]string) ([]dbus.ObjectPath, []dbus.ObjectPath, *dbus.Error)
SearchItems implements org.freedesktop.Secret.Service.SearchItems.
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 ¶
NewDHSession completes a Diffie-Hellman exchange with a client's public key, returning the session and the public key to send back.
func NewPlainSession ¶
NewPlainSession returns a session that sends secrets unencrypted.
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 ¶
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 ¶
Collections returns the collection names that exist.
func (*Store) CreateItem ¶
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 ¶
DeleteItem removes an item and its index entries.
func (*Store) Reindex ¶
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 ¶
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.