GODT-1525: Add keybase/go-keychain/secretservice as new keychain helper.

This commit is contained in:
Jakub
2022-03-18 15:25:02 +01:00
committed by Jakub Cuth
parent 5b20b6a3d0
commit c32a106898
4 changed files with 240 additions and 3 deletions

4
go.mod
View File

@ -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
)

4
go.sum
View File

@ -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=

View File

@ -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 <https://www.gnu.org/licenses/>.
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
}

View File

@ -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
}