diff --git a/go.mod b/go.mod index 19aca587..59b283c2 100644 --- a/go.mod +++ b/go.mod @@ -43,12 +43,13 @@ require ( github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect github.com/getsentry/sentry-go v0.12.0 github.com/go-resty/resty/v2 v2.6.0 + github.com/godbus/dbus v4.1.0+incompatible github.com/golang/mock v1.4.4 github.com/google/go-cmp v0.5.5 github.com/google/uuid v1.1.1 github.com/hashicorp/go-multierror v1.1.0 github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7 - github.com/keybase/go-keychain v0.0.0-20211119201326-e02f34051621 + github.com/keybase/go-keychain v0.0.0 github.com/kr/text v0.2.0 // indirect github.com/logrusorgru/aurora v2.0.3+incompatible github.com/mattn/go-runewidth v0.0.9 // indirect @@ -79,4 +80,5 @@ replace ( github.com/emersion/go-imap => github.com/ProtonMail/go-imap v0.0.0-20201228133358-4db68cea0cac github.com/emersion/go-message => github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753 github.com/jameskeane/bcrypt => github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57 + github.com/keybase/go-keychain => github.com/cuthix/go-keychain v0.0.0-20220405075754-31e7cee908fe ) diff --git a/go.sum b/go.sum index e12c82f5..f32cf0e6 100644 --- a/go.sum +++ b/go.sum @@ -102,6 +102,8 @@ github.com/cucumber/godog v0.12.1/go.mod h1:u6SD7IXC49dLpPN35kal0oYEjsXZWee4pW6T github.com/cucumber/messages-go/v16 v16.0.0/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g= github.com/cucumber/messages-go/v16 v16.0.1 h1:fvkpwsLgnIm0qugftrw2YwNlio+ABe2Iu94Ap8GMYIY= github.com/cucumber/messages-go/v16 v16.0.1/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g= +github.com/cuthix/go-keychain v0.0.0-20220405075754-31e7cee908fe h1:KRj3wdvA9yE92prNmOjS7x5DOqoyjxqdE30qnrmTasc= +github.com/cuthix/go-keychain v0.0.0-20220405075754-31e7cee908fe/go.mod h1:ZoZU1fnBy3mOLWr3Pg+Y2+nTKtu6ypDte2kZg9HvSwY= github.com/danieljoos/wincred v1.1.0 h1:3RNcEpBg4IhIChZdFRSdlQt1QjCp1sMAPIrOnm7Yf8g= github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -164,6 +166,8 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4= +github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= diff --git a/pkg/keychain/helper_dbus_linux.go b/pkg/keychain/helper_dbus_linux.go new file mode 100644 index 00000000..ffc4a849 --- /dev/null +++ b/pkg/keychain/helper_dbus_linux.go @@ -0,0 +1,220 @@ +// Copyright (c) 2022 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail 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. +// +// ProtonMail 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 ProtonMail Bridge. If not, see . + +package keychain + +import ( + "strings" + + "github.com/docker/docker-credential-helpers/credentials" + "github.com/godbus/dbus" + "github.com/keybase/go-keychain/secretservice" +) + +const ( + serverAtt = "server" + labelAtt = "label" + usernameAtt = "username" + + defaulDomain = "protonmail/bridge/users/" + defaultLabel = "Docker Credentials" +) + +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 items, 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, + "xdg:schema": "io.docker.Credentials", + "docker_cli": "1", + } + + 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 len(items) == 0 || err != nil { + return "", "", err + } + + 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 + } + + for _, it := range items { + attributes, err := service.GetAttributes(it) + if err != nil { + return nil, err + } + + if !strings.HasPrefix(attributes[serverAtt], defaulDomain) { + continue + } + + userIDByURL[attributes[serverAtt]] = attributes[usernameAtt] + } + + return userIDByURL, nil +} diff --git a/pkg/keychain/helper_linux.go b/pkg/keychain/helper_linux.go index 53b4c658..2a517ae6 100644 --- a/pkg/keychain/helper_linux.go +++ b/pkg/keychain/helper_linux.go @@ -28,13 +28,18 @@ import ( ) const ( - Pass = "pass-app" - SecretService = "secret-service" + Pass = "pass-app" + SecretService = "secret-service" + SecretServiceDBus = "secret-service-dbus" ) func init() { // nolint[noinit] Helpers = make(map[string]helperConstructor) + if isUsable(newDBusHelper("")) { + Helpers[SecretServiceDBus] = newDBusHelper + } + if _, err := exec.LookPath("gnome-keyring"); err == nil && isUsable(newSecretServiceHelper("")) { Helpers[SecretService] = newSecretServiceHelper } @@ -43,6 +48,8 @@ func init() { // nolint[noinit] Helpers[Pass] = newPassHelper } + defaultHelper = SecretServiceDBus + // If Pass is available, use it by default. // Otherwise, if SecretService is available, use it by default. if _, ok := Helpers[Pass]; ok { @@ -52,6 +59,10 @@ func init() { // nolint[noinit] } } +func newDBusHelper(string) (credentials.Helper, error) { + return &SecretServiceDBusHelper{}, nil +} + func newPassHelper(string) (credentials.Helper, error) { return &pass.Pass{}, nil }