178 lines
4.7 KiB
Go
178 lines
4.7 KiB
Go
//go:build (dragonfly && cgo) || (freebsd && cgo) || linux || netbsd || openbsd
|
|
|
|
package secret
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/godbus/dbus/v5"
|
|
)
|
|
|
|
const (
|
|
dbusServiceName = "org.freedesktop.secrets"
|
|
dbusServicePath = "/org/freedesktop/secrets"
|
|
dbusServiceInterface = "org.freedesktop.Secret.Service"
|
|
dbusServiceClose = dbusServiceInterface + ".Close"
|
|
dbusServiceUnlock = dbusServiceInterface + ".Unlock"
|
|
dbusCollectionBasePath = "/org/freedesktop/secrets/collection/"
|
|
dbusCollectionLoginPath = dbusCollectionBasePath + "login"
|
|
dbusCollectionLoginAliasPath = "/org/freedesktop/secrets/aliases/default"
|
|
dbusCollectionInterface = "org.freedesktop.Secret.Collection"
|
|
dbusCollectionGetSecret = dbusCollectionInterface + ".GetSecret"
|
|
dbusCollectionSearchItems = dbusCollectionInterface + ".SearchItems"
|
|
dbusPromptInterface = "org.freedesktop.Secret.Prompt"
|
|
dbusPrompt = dbusPromptInterface + ".Prompt"
|
|
dbusPromptCompleted = dbusPromptInterface + ".Completed"
|
|
)
|
|
|
|
type keyring struct {
|
|
service string
|
|
session *dbus.Conn
|
|
object dbus.BusObject
|
|
}
|
|
|
|
type keyringSecret struct {
|
|
Session dbus.ObjectPath
|
|
Parameters []byte
|
|
Value []byte
|
|
ContentType string `dbus:"content_type"`
|
|
}
|
|
|
|
func keyringProvider(service string) (Provider, error) {
|
|
session, err := dbus.SessionBus()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &keyring{
|
|
service: service,
|
|
session: session,
|
|
object: session.Object(dbusServiceName, dbusServicePath),
|
|
}, nil
|
|
}
|
|
|
|
func (p *keyring) Close() error {
|
|
return p.object.Call(dbusServiceClose, 0).Err
|
|
}
|
|
|
|
func (p *keyring) GetSecret(key string) (value []byte, err error) {
|
|
|
|
collection := p.getLoginCollection()
|
|
if err = p.unlock(collection.Path()); err != nil {
|
|
return
|
|
}
|
|
|
|
query := map[string]string{
|
|
"service": p.service,
|
|
"username": key,
|
|
}
|
|
|
|
var results []dbus.ObjectPath
|
|
if results, err = p.search(collection, query); err != nil {
|
|
return
|
|
} else if len(results) == 0 {
|
|
return nil, NotFound{Key: key}
|
|
}
|
|
|
|
var secret *keyringSecret
|
|
if secret, err = p.getSecret(results[0]); err != nil {
|
|
return
|
|
}
|
|
return secret.Value, nil
|
|
}
|
|
|
|
func (p keyring) checkCollectionPath(here dbus.ObjectPath) bool {
|
|
obj := p.session.Object(dbusServiceName, dbusServicePath)
|
|
val, err := obj.GetProperty(dbusCollectionInterface)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
paths := val.Value().([]dbus.ObjectPath)
|
|
for _, p := range paths {
|
|
if p == here {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (p keyring) getLoginCollection() dbus.BusObject {
|
|
here := dbus.ObjectPath(dbusCollectionLoginPath)
|
|
if !p.checkCollectionPath(here) {
|
|
here = dbus.ObjectPath(dbusCollectionLoginAliasPath)
|
|
}
|
|
return p.session.Object(dbusServiceName, here)
|
|
}
|
|
|
|
func (p *keyring) search(collection dbus.BusObject, query interface{}) (results []dbus.ObjectPath, err error) {
|
|
err = collection.Call(dbusCollectionSearchItems, 0, query).Store(&results)
|
|
return
|
|
}
|
|
|
|
func (p *keyring) getSecret(here dbus.ObjectPath) (secret *keyringSecret, err error) {
|
|
secret = new(keyringSecret)
|
|
err = p.session.Object(dbusServiceName, here).Call(dbusCollectionGetSecret, 0).Store(secret)
|
|
return
|
|
}
|
|
|
|
func (p *keyring) unlock(here dbus.ObjectPath) (err error) {
|
|
var (
|
|
unlocked []dbus.ObjectPath
|
|
prompt dbus.ObjectPath
|
|
)
|
|
if err = p.object.Call(dbusServiceUnlock, 0, []dbus.ObjectPath{here}).Store(&unlocked, &prompt); err != nil {
|
|
return
|
|
}
|
|
|
|
var variant dbus.Variant
|
|
if _, variant, err = p.handlePrompt(prompt); err != nil {
|
|
return
|
|
}
|
|
|
|
collections := variant.Value()
|
|
switch t := collections.(type) {
|
|
case []dbus.ObjectPath:
|
|
unlocked = append(unlocked, t...)
|
|
}
|
|
|
|
if len(unlocked) != 1 || (here != dbusCollectionLoginAliasPath && unlocked[0] != here) {
|
|
return fmt.Errorf("secret: failed to unlock keyring collection %q", here)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (s *keyring) handlePrompt(prompt dbus.ObjectPath) (bool, dbus.Variant, error) {
|
|
if prompt != dbus.ObjectPath("/") {
|
|
err := s.session.AddMatchSignal(dbus.WithMatchObjectPath(prompt),
|
|
dbus.WithMatchInterface(dbusPromptInterface),
|
|
)
|
|
if err != nil {
|
|
return false, dbus.MakeVariant(""), err
|
|
}
|
|
|
|
defer func(s *keyring, options ...dbus.MatchOption) {
|
|
_ = s.session.RemoveMatchSignal(options...)
|
|
}(s, dbus.WithMatchObjectPath(prompt), dbus.WithMatchInterface(dbusPromptInterface))
|
|
|
|
promptSignal := make(chan *dbus.Signal, 1)
|
|
s.session.Signal(promptSignal)
|
|
|
|
err = s.session.Object(dbusServiceName, prompt).Call(dbusPrompt, 0, "").Err
|
|
if err != nil {
|
|
return false, dbus.MakeVariant(""), err
|
|
}
|
|
|
|
signal := <-promptSignal
|
|
switch signal.Name {
|
|
case dbusPromptCompleted:
|
|
dismissed := signal.Body[0].(bool)
|
|
result := signal.Body[1].(dbus.Variant)
|
|
return dismissed, result, nil
|
|
}
|
|
|
|
}
|
|
|
|
return false, dbus.MakeVariant(""), nil
|
|
}
|