Skip to content
This repository has been archived by the owner on Feb 16, 2023. It is now read-only.

Improve Credential Errors #223

Merged
merged 16 commits into from
Dec 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions pkg/secrethub/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ type Client struct {
// These are cached
repoIndexKeys map[api.RepoPath]*crypto.SymmetricKey

defaultPassphraseReader credentials.Reader

appInfo []*AppInfo
ConfigDir *configdir.Dir
}
Expand Down Expand Up @@ -119,9 +121,10 @@ func (i AppInfo) ValidateName() error {
// If no key credential could be found, a Client is returned that can only be used for unauthenticated routes.
func NewClient(with ...ClientOption) (*Client, error) {
client := &Client{
httpClient: http.NewClient(),
repoIndexKeys: make(map[api.RepoPath]*crypto.SymmetricKey),
appInfo: []*AppInfo{},
httpClient: http.NewClient(),
repoIndexKeys: make(map[api.RepoPath]*crypto.SymmetricKey),
appInfo: []*AppInfo{},
defaultPassphraseReader: credentials.FromEnv("SECRETHUB_CREDENTIAL_PASSPHRASE"),
}
err := client.with(with...)
if err != nil {
Expand All @@ -144,7 +147,7 @@ func NewClient(with ...ClientOption) (*Client, error) {
var provider credentials.Provider
switch strings.ToLower(identityProvider) {
case "", "key":
provider = credentials.UseKey(client.DefaultCredential())
provider = credentials.UseKey(client.DefaultCredential()).Passphrase(client.defaultPassphraseReader)
case "aws":
provider = credentials.UseAWS()
case "gcp":
Expand Down Expand Up @@ -264,9 +267,10 @@ func (c *Client) with(options ...ClientOption) error {
// sourcing it either from the SECRETHUB_CREDENTIAL environment variable or
// from the configuration directory.
func (c *Client) DefaultCredential() credentials.Reader {
envCredential := os.Getenv("SECRETHUB_CREDENTIAL")
const credentialEnvironmentVariable = "SECRETHUB_CREDENTIAL"
envCredential := os.Getenv(credentialEnvironmentVariable)
if envCredential != "" {
return credentials.FromString(envCredential)
return credentials.FromEnv(credentialEnvironmentVariable)
}

return c.ConfigDir.Credential()
Expand Down
9 changes: 9 additions & 0 deletions pkg/secrethub/client_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,12 @@ func WithCredentials(provider credentials.Provider) ClientOption {
return nil
}
}

// WithDefaultPassphraseReader sets a default passphrase reader that is used for decrypting an encrypted key credential
// if no credential is set explicitly.
func WithDefaultPassphraseReader(reader credentials.Reader) ClientOption {
return func(c *Client) error {
c.defaultPassphraseReader = reader
return nil
}
}
5 changes: 5 additions & 0 deletions pkg/secrethub/configdir/dir.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ func (f *CredentialFile) Path() string {
return f.path
}

// Source returns the path to the credential file.
func (f *CredentialFile) Source() string {
return f.path
}

// Write writes the given bytes to the credential file.
func (f *CredentialFile) Write(data []byte) error {
err := os.MkdirAll(filepath.Dir(f.path), os.FileMode(0700))
Expand Down
1 change: 1 addition & 0 deletions pkg/secrethub/credentials/encoding.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ var (
ErrCannotDecodeCredentialPayload = errCredentials.Code("invalid_credential_header").ErrorPref("cannot decode credential payload: %v")
ErrCannotDecodeEncryptedCredential = errCredentials.Code("cannot_decode_encrypted_credential").Error("cannot decode an encrypted credential without a key")
ErrCannotDecryptCredential = errCredentials.Code("cannot_decrypt_credential").Error("passphrase is incorrect")
ErrNeedPassphrase = errCredentials.Code("credential_passphrase_required").Error("credential is password-protected. Configure a credential passphrase through the SECRETHUB_CREDENTIAL_PASSPHRASE environment variable or use a credential that is not password-protected")
ErrMalformedCredential = errCredentials.Code("malformed_credential").ErrorPref("credential is malformed: %v")
ErrInvalidKey = errCredentials.Code("invalid_key").Error("the given key is not valid for the encryption algorithm")
)
Expand Down
22 changes: 21 additions & 1 deletion pkg/secrethub/credentials/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,23 @@ package credentials

import (
"errors"
"fmt"
"os"

"github.com/secrethub/secrethub-go/internals/auth"
"github.com/secrethub/secrethub-go/internals/crypto"
"github.com/secrethub/secrethub-go/pkg/secrethub/internals/http"
)

type ErrLoadingCredential struct {
Location string
Err error
}

func (e ErrLoadingCredential) Error() string {
return "load credential " + e.Location + ": " + e.Err.Error()
}

// Key is a credential that uses a local key for all its operations.
type Key struct {
key *RSACredential
Expand Down Expand Up @@ -69,8 +80,17 @@ func ImportKey(credentialReader, passphraseReader Reader) (Key, error) {
return Key{}, err
}
if encoded.IsEncrypted() {
const credentialPassphraseEnvVar = "SECRETHUB_CREDENTIAL_PASSPHRASE"
envPassphrase := os.Getenv(credentialPassphraseEnvVar)
if envPassphrase != "" {
credential, err := decryptKey([]byte(envPassphrase), encoded)
if err != nil {
return Key{}, fmt.Errorf("decrypting credential with passphrase read from $%s: %v", credentialPassphraseEnvVar, err)
}
return Key{key: credential}, nil
}
if passphraseReader == nil {
return Key{}, errors.New("need passphrase")
return Key{}, ErrNeedPassphrase
}

// Try up to three times to get the correct passphrase.
Expand Down
12 changes: 12 additions & 0 deletions pkg/secrethub/credentials/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ func (k KeyProvider) Passphrase(passphraseReader Reader) Provider {
func (k KeyProvider) Provide(httpClient *http.Client) (auth.Authenticator, Decrypter, error) {
key, err := ImportKey(k.credentialReader, k.passphraseReader)
if err != nil {
if source, ok := k.credentialReader.(CredentialSource); ok {
return nil, nil, ErrLoadingCredential{
Location: source.Source(),
Err: err,
}
}
return nil, nil, err
}
return key.Provide(httpClient)
Expand All @@ -52,3 +58,9 @@ type providerFunc func(*http.Client) (auth.Authenticator, Decrypter, error)
func (f providerFunc) Provide(httpClient *http.Client) (auth.Authenticator, Decrypter, error) {
return f(httpClient)
}

// CredentialSource should be implemented by credential readers to allow returning credential reading errors
// that include the credentials source (e.g. path to credential file, environment variable etc.).
type CredentialSource interface {
Source() string
}
30 changes: 25 additions & 5 deletions pkg/secrethub/credentials/readers.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ type Reader interface {
// FromFile returns a reader that reads the contents of a file,
// e.g. a credential or a passphrase.
func FromFile(path string) Reader {
return readerFunc(func() ([]byte, error) {
return newReader(path, func() ([]byte, error) {
return ioutil.ReadFile(path)
})
}

// FromEnv returns a reader that reads the contents of an
// environment variable, e.g. a credential or a passphrase.
func FromEnv(key string) Reader {
return readerFunc(func() ([]byte, error) {
return newReader("$"+key, func() ([]byte, error) {
return []byte(os.Getenv(key)), nil
})
}
Expand All @@ -43,10 +43,30 @@ func FromString(raw string) Reader {
})
}

// readerFunc is a helper function to create a reader from any func() ([]byte, error).
type readerFunc func() ([]byte, error)

func (r readerFunc) Read() ([]byte, error) {
return r()
}

// newReader is a helper function to create a reader with a source from any func() ([]byte, error).
func newReader(source string, read func() ([]byte, error)) Reader {
return reader{
source: source,
readFunc: read,
}
}

type reader struct {
source string
readFunc func() ([]byte, error)
}

func (r reader) Source() string {
return r.source
}

// Read implements the Reader interface.
func (f readerFunc) Read() ([]byte, error) {
return f()
func (r reader) Read() ([]byte, error) {
return r.readFunc()
}