343 lines
8.2 KiB
Go
343 lines
8.2 KiB
Go
package kwallet
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/godbus/dbus/v5"
|
|
)
|
|
|
|
const (
|
|
kwalletInterface = "org.kde.KWallet"
|
|
kwalletAppID = "email-mcp"
|
|
kwalletFolderName = "email-mcp"
|
|
)
|
|
|
|
var (
|
|
ErrKWalletUnavailable = errors.New("kwallet is not available")
|
|
ErrKWalletDisabled = errors.New("kwallet is disabled")
|
|
ErrKWalletOpenFailed = errors.New("kwallet could not be opened")
|
|
ErrCredentialNotFound = errors.New("credentials not configured")
|
|
)
|
|
|
|
type ClientImpl struct {
|
|
probe func(context.Context) error
|
|
open func(context.Context) error
|
|
read func(context.Context, string) ([]byte, error)
|
|
write func(context.Context, string, []byte) error
|
|
}
|
|
|
|
type dbusConnection interface {
|
|
Object(dest string, path dbus.ObjectPath) dbusObject
|
|
}
|
|
|
|
type dbusObject interface {
|
|
CallWithContext(ctx context.Context, method string, flags dbus.Flags, args ...any) *dbus.Call
|
|
}
|
|
|
|
type kwalletService struct {
|
|
name string
|
|
path dbus.ObjectPath
|
|
}
|
|
|
|
type walletSession struct {
|
|
connect func() (dbusConnection, error)
|
|
|
|
services []kwalletService
|
|
object dbusObject
|
|
handle int32
|
|
opened bool
|
|
}
|
|
|
|
type sessionBusConnection struct {
|
|
conn *dbus.Conn
|
|
}
|
|
|
|
type sessionBusObject struct {
|
|
object dbus.BusObject
|
|
}
|
|
|
|
func NewDefaultWalletClient() Client {
|
|
return newClientImpl(newWalletSession(defaultSessionBusConnection))
|
|
}
|
|
|
|
func newClientImpl(session *walletSession) ClientImpl {
|
|
return ClientImpl{
|
|
probe: session.probe,
|
|
open: session.open,
|
|
read: session.readEntry,
|
|
write: session.writeEntry,
|
|
}
|
|
}
|
|
|
|
func newWalletSession(connect func() (dbusConnection, error)) *walletSession {
|
|
return &walletSession{
|
|
connect: connect,
|
|
services: []kwalletService{
|
|
{name: "org.kde.kwalletd6", path: "/modules/kwalletd6"},
|
|
{name: "org.kde.kwalletd5", path: "/modules/kwalletd5"},
|
|
},
|
|
}
|
|
}
|
|
|
|
func defaultSessionBusConnection() (dbusConnection, error) {
|
|
conn, err := dbus.SessionBus()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: %v", ErrKWalletUnavailable, err)
|
|
}
|
|
return sessionBusConnection{conn: conn}, nil
|
|
}
|
|
|
|
func (c sessionBusConnection) Object(dest string, path dbus.ObjectPath) dbusObject {
|
|
return sessionBusObject{object: c.conn.Object(dest, path)}
|
|
}
|
|
|
|
func (o sessionBusObject) CallWithContext(ctx context.Context, method string, flags dbus.Flags, args ...any) *dbus.Call {
|
|
return o.object.CallWithContext(ctx, method, flags, args...)
|
|
}
|
|
|
|
func (c ClientImpl) IsAvailable(ctx context.Context) error {
|
|
if c.probe == nil {
|
|
return ErrKWalletUnavailable
|
|
}
|
|
return c.probe(ctx)
|
|
}
|
|
|
|
func (c ClientImpl) Open(ctx context.Context) error {
|
|
if c.open == nil {
|
|
return nil
|
|
}
|
|
return c.open(ctx)
|
|
}
|
|
|
|
func (c ClientImpl) WriteEntry(ctx context.Context, key string, value []byte) error {
|
|
if c.write == nil {
|
|
return fmt.Errorf("kwallet write operation is not configured")
|
|
}
|
|
return c.write(ctx, key, value)
|
|
}
|
|
|
|
func (c ClientImpl) ReadEntry(ctx context.Context, key string) ([]byte, error) {
|
|
if c.read == nil {
|
|
return nil, ErrCredentialNotFound
|
|
}
|
|
return c.read(ctx, key)
|
|
}
|
|
|
|
func (s *walletSession) probe(ctx context.Context) error {
|
|
_, err := s.ensureObject(ctx)
|
|
return err
|
|
}
|
|
|
|
func (s *walletSession) open(ctx context.Context) error {
|
|
object, err := s.ensureObject(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if s.opened {
|
|
return nil
|
|
}
|
|
|
|
walletName, err := s.walletName(ctx, object)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %v", ErrKWalletOpenFailed, err)
|
|
}
|
|
|
|
handle, err := s.callInt32(ctx, object, "open", walletName, int64(0), kwalletAppID)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %v", ErrKWalletOpenFailed, err)
|
|
}
|
|
if handle < 0 {
|
|
return fmt.Errorf("%w: handle %d", ErrKWalletOpenFailed, handle)
|
|
}
|
|
|
|
hasFolder, err := s.callBool(ctx, object, "hasFolder", handle, kwalletFolderName, kwalletAppID)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %v", ErrKWalletOpenFailed, err)
|
|
}
|
|
if !hasFolder {
|
|
created, err := s.callBool(ctx, object, "createFolder", handle, kwalletFolderName, kwalletAppID)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %v", ErrKWalletOpenFailed, err)
|
|
}
|
|
if !created {
|
|
return fmt.Errorf("%w: folder %q could not be created", ErrKWalletOpenFailed, kwalletFolderName)
|
|
}
|
|
}
|
|
|
|
s.handle = handle
|
|
s.opened = true
|
|
return nil
|
|
}
|
|
|
|
func (s *walletSession) writeEntry(ctx context.Context, key string, value []byte) error {
|
|
if err := s.open(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
object, err := s.ensureObject(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
code, err := s.callInt32(ctx, object, "writeEntry", s.handle, kwalletFolderName, key, value, kwalletAppID)
|
|
if err != nil {
|
|
return s.wrapUnavailable("kwallet write failed", err)
|
|
}
|
|
if code != 0 {
|
|
return fmt.Errorf("kwallet write failed with code %d", code)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *walletSession) readEntry(ctx context.Context, key string) ([]byte, error) {
|
|
if err := s.open(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
object, err := s.ensureObject(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
hasEntry, err := s.callBool(ctx, object, "hasEntry", s.handle, kwalletFolderName, key, kwalletAppID)
|
|
if err != nil {
|
|
return nil, s.wrapUnavailable("kwallet entry lookup failed", err)
|
|
}
|
|
if !hasEntry {
|
|
return nil, fmt.Errorf("%w: key %q", ErrCredentialNotFound, key)
|
|
}
|
|
|
|
value, err := s.callBytes(ctx, object, "readEntry", s.handle, kwalletFolderName, key, kwalletAppID)
|
|
if err != nil {
|
|
return nil, s.wrapUnavailable("kwallet read failed", err)
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func (s *walletSession) ensureObject(ctx context.Context) (dbusObject, error) {
|
|
if s.object != nil {
|
|
return s.object, nil
|
|
}
|
|
|
|
conn, err := s.connect()
|
|
if err != nil {
|
|
if errors.Is(err, ErrKWalletUnavailable) {
|
|
return nil, err
|
|
}
|
|
return nil, fmt.Errorf("%w: %v", ErrKWalletUnavailable, err)
|
|
}
|
|
|
|
var lastErr error
|
|
for _, service := range s.services {
|
|
object := conn.Object(service.name, service.path)
|
|
enabled, err := s.callBool(ctx, object, "isEnabled")
|
|
if err != nil {
|
|
lastErr = err
|
|
continue
|
|
}
|
|
if !enabled {
|
|
return nil, ErrKWalletDisabled
|
|
}
|
|
|
|
s.object = object
|
|
return s.object, nil
|
|
}
|
|
|
|
if lastErr == nil {
|
|
lastErr = errors.New("no kwallet service responded")
|
|
}
|
|
return nil, fmt.Errorf("%w: %v", ErrKWalletUnavailable, lastErr)
|
|
}
|
|
|
|
func (s *walletSession) walletName(ctx context.Context, object dbusObject) (string, error) {
|
|
name, err := s.callString(ctx, object, "networkWallet")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
name = strings.TrimSpace(name)
|
|
if name != "" {
|
|
return name, nil
|
|
}
|
|
|
|
name, err = s.callString(ctx, object, "localWallet")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
name = strings.TrimSpace(name)
|
|
if name == "" {
|
|
return "", errors.New("kwallet did not report a wallet name")
|
|
}
|
|
return name, nil
|
|
}
|
|
|
|
func (s *walletSession) callBool(ctx context.Context, object dbusObject, name string, args ...any) (bool, error) {
|
|
var value bool
|
|
if err := object.CallWithContext(ctx, kwalletMethod(name), 0, args...).Store(&value); err != nil {
|
|
return false, err
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func (s *walletSession) callInt32(ctx context.Context, object dbusObject, name string, args ...any) (int32, error) {
|
|
var value int32
|
|
if err := object.CallWithContext(ctx, kwalletMethod(name), 0, args...).Store(&value); err != nil {
|
|
return 0, err
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func (s *walletSession) callString(ctx context.Context, object dbusObject, name string, args ...any) (string, error) {
|
|
var value string
|
|
if err := object.CallWithContext(ctx, kwalletMethod(name), 0, args...).Store(&value); err != nil {
|
|
return "", err
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func (s *walletSession) callBytes(ctx context.Context, object dbusObject, name string, args ...any) ([]byte, error) {
|
|
var value []byte
|
|
if err := object.CallWithContext(ctx, kwalletMethod(name), 0, args...).Store(&value); err != nil {
|
|
return nil, err
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func (s *walletSession) reset() {
|
|
s.object = nil
|
|
s.handle = 0
|
|
s.opened = false
|
|
}
|
|
|
|
func kwalletMethod(name string) string {
|
|
return kwalletInterface + "." + name
|
|
}
|
|
|
|
type typedError struct {
|
|
message string
|
|
errs []error
|
|
}
|
|
|
|
func (e *typedError) Error() string {
|
|
return e.message
|
|
}
|
|
|
|
func (e *typedError) Unwrap() []error {
|
|
return e.errs
|
|
}
|
|
|
|
func (s *walletSession) wrapUnavailable(message string, err error) error {
|
|
if err == nil {
|
|
return err
|
|
}
|
|
s.reset()
|
|
if errors.Is(err, ErrKWalletUnavailable) {
|
|
return err
|
|
}
|
|
return &typedError{
|
|
message: fmt.Sprintf("%s: %v", message, err),
|
|
errs: []error{ErrKWalletUnavailable, err},
|
|
}
|
|
}
|