--- pkg/keychain/helper_dbus_freebsd.go.orig 2026-07-26 08:26:30 UTC +++ pkg/keychain/helper_dbus_freebsd.go @@ -0,0 +1,230 @@ +// Copyright (c) 2026 Proton AG +// +// This file is part of Proton Mail Bridge. +// +// Proton Mail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Proton Mail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Proton Mail Bridge. If not, see . + +package keychain + +import ( + "strings" + + "github.com/ProtonMail/proton-bridge/v3/internal/constants" + "github.com/ProtonMail/proton-bridge/v3/pkg/utils" + "github.com/docker/docker-credential-helpers/credentials" + "github.com/godbus/dbus" + "github.com/keybase/go-keychain/secretservice" +) + +const ( + serverAtt = "server" + labelAtt = "label" + usernameAtt = "username" + + defaultLabel = "Proton Mail Bridge Credentials" +) + +func getDomain() string { + return hostURL(constants.KeyChainName) +} + +func getSession() (*secretservice.SecretService, *secretservice.Session, error) { + service, err := secretservice.NewService() + if err != nil { + return nil, nil, err + } + + session, err := service.OpenSession(secretservice.AuthenticationDHAES) + if err != nil { + return nil, nil, err + } + + return service, session, nil +} + +func handleTimeout(f func() error) error { + err := f() + if err == secretservice.ErrPromptTimedOut { + return f() + } + return err +} + +func getItems(service *secretservice.SecretService, attributes map[string]string) ([]dbus.ObjectPath, error) { + if err := unlock(service); err != nil { + return nil, err + } + + var items []dbus.ObjectPath + err := handleTimeout(func() error { + var err error + items, err = service.SearchCollection( + secretservice.DefaultCollection, + attributes, + ) + return err + }) + if err != nil { + return nil, err + } + return utils.Filter(items, func(t dbus.ObjectPath) bool { + return strings.HasPrefix(string(t), "/org/freedesktop/secrets") + }), err +} + +func unlock(service *secretservice.SecretService) error { + return handleTimeout(func() error { + return service.Unlock([]dbus.ObjectPath{secretservice.DefaultCollection}) + }) +} + +// SecretServiceDBusHelper is wrapper around keybase/go-keychain/secretservice +// library. +type SecretServiceDBusHelper struct{} + +// Add appends credentials to the store. +func (s *SecretServiceDBusHelper) Add(creds *credentials.Credentials) error { + service, session, err := getSession() + if err != nil { + return err + } + defer service.CloseSession(session) + + if err := unlock(service); err != nil { + return err + } + + secret, err := session.NewSecret([]byte(creds.Secret)) + if err != nil { + return err + } + + attributes := map[string]string{ + usernameAtt: creds.Username, + serverAtt: creds.ServerURL, + labelAtt: defaultLabel, + } + + return handleTimeout(func() error { + _, err = service.CreateItem( + secretservice.DefaultCollection, + secretservice.NewSecretProperties(creds.ServerURL, attributes), + secret, + secretservice.ReplaceBehaviorReplace, + ) + + return err + }) +} + +// Delete removes credentials from the store. +func (s *SecretServiceDBusHelper) Delete(serverURL string) error { + service, session, err := getSession() + if err != nil { + return err + } + defer service.CloseSession(session) + + items, err := getItems(service, map[string]string{ + labelAtt: defaultLabel, + serverAtt: serverURL, + }) + + if len(items) == 0 || err != nil { + return err + } + + return handleTimeout(func() error { + return service.DeleteItem(items[0]) + }) +} + +// Get retrieves credentials from the store. +// It returns username and secret as strings. +func (s *SecretServiceDBusHelper) Get(serverURL string) (string, string, error) { + service, session, err := getSession() + if err != nil { + return "", "", err + } + defer service.CloseSession(session) + + if err := unlock(service); err != nil { + return "", "", err + } + + items, err := getItems(service, map[string]string{ + labelAtt: defaultLabel, + serverAtt: serverURL, + }) + + if err != nil { + return "", "", err + } + + if len(items) == 0 { + return "", "", ErrKeychainNoItem + } + + item := items[0] + + attributes, err := service.GetAttributes(item) + if err != nil { + return "", "", err + } + + var secretPlaintext []byte + err = handleTimeout(func() error { + var err error + secretPlaintext, err = service.GetSecret(item, *session) + return err + }) + if err != nil { + return "", "", err + } + + return attributes[usernameAtt], string(secretPlaintext), nil +} + +// List returns the stored serverURLs and their associated usernames. +func (s *SecretServiceDBusHelper) List() (map[string]string, error) { + userIDByURL := make(map[string]string) + + service, session, err := getSession() + if err != nil { + return nil, err + } + defer service.CloseSession(session) + + items, err := getItems(service, map[string]string{labelAtt: defaultLabel}) + if err != nil { + return nil, err + } + + defaultDomain := getDomain() + + for _, it := range items { + attributes, err := service.GetAttributes(it) + if err != nil { + return nil, err + } + + if !strings.HasPrefix(attributes[serverAtt], defaultDomain) { + continue + } + + userIDByURL[attributes[serverAtt]] = attributes[usernameAtt] + } + + return userIDByURL, nil +}