diff --git a/pkg/secrethub/client.go b/pkg/secrethub/client.go index 5e0802b7..1c73c11d 100644 --- a/pkg/secrethub/client.go +++ b/pkg/secrethub/client.go @@ -82,6 +82,8 @@ type Client struct { // These are cached repoIndexKeys map[api.RepoPath]*crypto.SymmetricKey + defaultPassphraseReader credentials.Reader + appInfo []*AppInfo ConfigDir *configdir.Dir } @@ -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 { @@ -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": @@ -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() diff --git a/pkg/secrethub/client_options.go b/pkg/secrethub/client_options.go index 7ad43796..2c867b8e 100644 --- a/pkg/secrethub/client_options.go +++ b/pkg/secrethub/client_options.go @@ -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 + } +} diff --git a/pkg/secrethub/configdir/dir.go b/pkg/secrethub/configdir/dir.go index 51b6106b..c8f83218 100644 --- a/pkg/secrethub/configdir/dir.go +++ b/pkg/secrethub/configdir/dir.go @@ -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)) diff --git a/pkg/secrethub/credentials/encoding.go b/pkg/secrethub/credentials/encoding.go index 3ed72ddf..0c22d047 100644 --- a/pkg/secrethub/credentials/encoding.go +++ b/pkg/secrethub/credentials/encoding.go @@ -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") ) diff --git a/pkg/secrethub/credentials/key.go b/pkg/secrethub/credentials/key.go index 211c2767..23e039d9 100644 --- a/pkg/secrethub/credentials/key.go +++ b/pkg/secrethub/credentials/key.go @@ -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 @@ -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. diff --git a/pkg/secrethub/credentials/providers.go b/pkg/secrethub/credentials/providers.go index 858160c2..c68d4dfd 100644 --- a/pkg/secrethub/credentials/providers.go +++ b/pkg/secrethub/credentials/providers.go @@ -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) @@ -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 +} diff --git a/pkg/secrethub/credentials/readers.go b/pkg/secrethub/credentials/readers.go index 67fe88bd..9d1fdaab 100644 --- a/pkg/secrethub/credentials/readers.go +++ b/pkg/secrethub/credentials/readers.go @@ -14,7 +14,7 @@ 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) }) } @@ -22,7 +22,7 @@ func FromFile(path string) Reader { // 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 }) } @@ -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() }