1
0
mirror of https://github.com/docker/docker-credential-helpers.git synced 2026-06-13 16:01:28 +05:30
Files
docker-credential-helpers/osxkeychain/osxkeychain.go
T
Albin Kerouanton b1d5bf0326 osxkeychain: list: return full server URIs
Commit 4cdcdc2 changed the format of `list` output. Before that commit,
the json keys were containing full URIs (scheme://host/path[:port]),
but afterward, the keys were only containing the path component.

With this commit, the `list` operation now returns full URIs (fixing the
regression), and also fixes the malformed URIs issue when a port is
specified (introduced by 19ec1c3, and affecting >=v0.4.2,<v0.9.0).

Signed-off-by: Albin Kerouanton <albinker@gmail.com>
2025-03-04 11:20:57 +01:00

172 lines
4.9 KiB
Go

//go:build darwin && cgo
package osxkeychain
/*
#cgo LDFLAGS: -framework Security -framework CoreFoundation
#include <CoreFoundation/CoreFoundation.h>
#include <Security/Security.h>
*/
import "C"
import (
"errors"
"net"
"net/url"
"strconv"
"github.com/docker/docker-credential-helpers/credentials"
"github.com/docker/docker-credential-helpers/registryurl"
"github.com/keybase/go-keychain"
)
// https://opensource.apple.com/source/Security/Security-55471/sec/Security/SecBase.h.auto.html
const (
// errCredentialsNotFound is the specific error message returned by OS X
// when the credentials are not in the keychain.
errCredentialsNotFound = "The specified item could not be found in the keychain. (-25300)"
// errInteractionNotAllowed is the specific error message returned by OS X
// when environment does not allow showing dialog to unlock keychain.
errInteractionNotAllowed = "User interaction is not allowed. (-25308)"
)
// ErrInteractionNotAllowed is returned if keychain password prompt can not be shown.
var ErrInteractionNotAllowed = errors.New(`keychain cannot be accessed because the current session does not allow user interaction. The keychain may be locked; unlock it by running "security -v unlock-keychain ~/Library/Keychains/login.keychain-db" and try again`)
// Osxkeychain handles secrets using the OS X Keychain as store.
type Osxkeychain struct{}
// Add adds new credentials to the keychain.
func (h Osxkeychain) Add(creds *credentials.Credentials) error {
_ = h.Delete(creds.ServerURL) // ignore errors as existing credential may not exist.
item := keychain.NewItem()
item.SetSecClass(keychain.SecClassInternetPassword)
item.SetLabel(credentials.CredsLabel)
item.SetAccount(creds.Username)
item.SetData([]byte(creds.Secret))
if err := splitServer(creds.ServerURL, item); err != nil {
return err
}
return keychain.AddItem(item)
}
// Delete removes credentials from the keychain.
func (h Osxkeychain) Delete(serverURL string) error {
item := keychain.NewItem()
item.SetSecClass(keychain.SecClassInternetPassword)
if err := splitServer(serverURL, item); err != nil {
return err
}
if err := keychain.DeleteItem(item); err != nil {
switch err.Error() {
case errCredentialsNotFound:
return credentials.NewErrCredentialsNotFound()
case errInteractionNotAllowed:
return ErrInteractionNotAllowed
default:
return err
}
}
return nil
}
// Get returns the username and secret to use for a given registry server URL.
func (h Osxkeychain) Get(serverURL string) (string, string, error) {
item := keychain.NewItem()
item.SetSecClass(keychain.SecClassInternetPassword)
item.SetMatchLimit(keychain.MatchLimitOne)
item.SetReturnAttributes(true)
item.SetReturnData(true)
if err := splitServer(serverURL, item); err != nil {
return "", "", err
}
res, err := keychain.QueryItem(item)
if err != nil {
switch err.Error() {
case errCredentialsNotFound:
return "", "", credentials.NewErrCredentialsNotFound()
case errInteractionNotAllowed:
return "", "", ErrInteractionNotAllowed
default:
return "", "", err
}
} else if len(res) == 0 {
return "", "", credentials.NewErrCredentialsNotFound()
}
return res[0].Account, string(res[0].Data), nil
}
// List returns the stored URLs and corresponding usernames.
func (h Osxkeychain) List() (map[string]string, error) {
item := keychain.NewItem()
item.SetSecClass(keychain.SecClassInternetPassword)
item.SetMatchLimit(keychain.MatchLimitAll)
item.SetReturnAttributes(true)
item.SetLabel(credentials.CredsLabel)
res, err := keychain.QueryItem(item)
if err != nil {
switch err.Error() {
case errCredentialsNotFound:
return make(map[string]string), nil
case errInteractionNotAllowed:
return nil, ErrInteractionNotAllowed
default:
return nil, err
}
}
resp := make(map[string]string)
for _, r := range res {
proto := "http"
if r.Protocol == kSecProtocolTypeHTTPS {
proto = "https"
}
host := r.Server
if r.Port != 0 {
host = net.JoinHostPort(host, strconv.Itoa(int(r.Port)))
}
u := url.URL{
Scheme: proto,
Host: host,
Path: r.Path,
}
resp[u.String()] = r.Account
}
return resp, nil
}
const (
// Hardcoded protocol types matching their Objective-C equivalents.
// https://developer.apple.com/documentation/security/ksecattrprotocolhttps?language=objc
kSecProtocolTypeHTTPS = "htps" // This is NOT a typo.
// https://developer.apple.com/documentation/security/ksecattrprotocolhttp?language=objc
kSecProtocolTypeHTTP = "http"
)
func splitServer(serverURL string, item keychain.Item) error {
u, err := registryurl.Parse(serverURL)
if err != nil {
return err
}
item.SetProtocol(kSecProtocolTypeHTTPS)
if u.Scheme == "http" {
item.SetProtocol(kSecProtocolTypeHTTP)
}
item.SetServer(u.Hostname())
if p := u.Port(); p != "" {
port, err := strconv.Atoi(p)
if err != nil {
return err
}
item.SetPort(int32(port))
}
item.SetPath(u.Path)
return nil
}