diff --git a/internals/aws/service_creator.go b/internals/aws/service_creator.go index 7fd853da..0caeabf8 100644 --- a/internals/aws/service_creator.go +++ b/internals/aws/service_creator.go @@ -69,8 +69,14 @@ func (c CredentialCreator) Type() api.CredentialType { } // Verifier returns the verifier of an AWS service. -func (c CredentialCreator) Verifier() ([]byte, error) { - return []byte(c.role), nil +func (c CredentialCreator) Export() ([]byte, string, error) { + verifier := []byte(c.role) + + fingerprint, err := api.GetFingerprint(c.Type(), verifier) + if err != nil { + return nil, "", err + } + return verifier, fingerprint, nil } // AddProof adds proof of access to the AWS account to the CreateCredentialRequest. diff --git a/internals/crypto/rsa.go b/internals/crypto/rsa.go index 0d79d335..6bdfe9be 100644 --- a/internals/crypto/rsa.go +++ b/internals/crypto/rsa.go @@ -145,7 +145,7 @@ func Verify(encodedPublicKey, message, signature []byte) error { // Fingerprint returns the SHA256 hash of the public key, encoded as a hexadecimal string. func (pub RSAPublicKey) Fingerprint() (string, error) { - exported, err := pub.Export() + exported, err := pub.Encode() if err != nil { return "", errio.Error(err) } @@ -154,9 +154,9 @@ func (pub RSAPublicKey) Fingerprint() (string, error) { return hex.EncodeToString(sum[:]), nil } -// Export uses PEM encoding to encode the public key as bytes so it +// Encode uses PEM encoding to encode the public key as bytes so it // can be easily stored and transferred between systems. -func (pub RSAPublicKey) Export() ([]byte, error) { +func (pub RSAPublicKey) Encode() ([]byte, error) { asn1, err := x509.MarshalPKIXPublicKey(pub.publicKey) if err != nil { return nil, errio.Error(err) @@ -295,7 +295,7 @@ func (prv RSAPrivateKey) unwrap(encryptedData []byte) ([]byte, error) { } // Export returns the private key in ASN.1 DER encoded format. -func (prv RSAPrivateKey) Export() []byte { +func (prv RSAPrivateKey) Encode() []byte { return x509.MarshalPKCS1PrivateKey(prv.private) } diff --git a/internals/crypto/rsa_test.go b/internals/crypto/rsa_test.go index 1b906610..e408da45 100644 --- a/internals/crypto/rsa_test.go +++ b/internals/crypto/rsa_test.go @@ -107,7 +107,7 @@ func TestSign_Verify(t *testing.T) { t.Error(err) } - pk, _ := key1.Public().Export() + pk, _ := key1.Public().Encode() err = Verify(pk, message, signature) if err != nil { @@ -118,7 +118,7 @@ func TestSign_Verify(t *testing.T) { func TestImport_Exported_PublicKey(t *testing.T) { key1 := getTestKey1(t) - exportedPublicKey, err := key1.Public().Export() + exportedPublicKey, err := key1.Public().Encode() if err != nil { t.Error(err) } @@ -135,7 +135,7 @@ func TestImport_Exported_ServiceKey(t *testing.T) { t.Errorf("generateServiceKey generates error: %s", err) } - public, err := clientKey.Public().Export() + public, err := clientKey.Public().Encode() if err != nil { t.Errorf("cannot import generated public key: %s", err) } diff --git a/pkg/secrethub/account.go b/pkg/secrethub/account.go index d84f0e6f..bcd67c2c 100644 --- a/pkg/secrethub/account.go +++ b/pkg/secrethub/account.go @@ -50,7 +50,7 @@ func (s accountService) Keys() AccountKeyService { // The intermediate key is returned in an CreateAccountKeyRequest ready to be sent to the API. // If an error has occurred, it will be returned and the other result should be considered invalid. func (c *Client) createAccountKeyRequest(encrypter credentials.Encrypter, accountKey crypto.RSAPrivateKey) (*api.CreateAccountKeyRequest, error) { - publicAccountKey, err := accountKey.Public().Export() + publicAccountKey, err := accountKey.Public().Encode() if err != nil { return nil, errio.Error(err) } @@ -72,14 +72,10 @@ func (c *Client) createAccountKeyRequest(encrypter credentials.Encrypter, accoun } func (c *Client) createCredentialRequest(verifier credentials.Verifier, metadata map[string]string) (*api.CreateCredentialRequest, error) { - bytes, err := verifier.Verifier() + bytes, fingerprint, err := verifier.Export() if err != nil { return nil, errio.Error(err) } - fingerprint, err := api.GetFingerprint(verifier.Type(), bytes) - if err != nil { - return nil, err - } req := api.CreateCredentialRequest{ Fingerprint: fingerprint, diff --git a/pkg/secrethub/client.go b/pkg/secrethub/client.go index fbaf0765..d5f74be7 100644 --- a/pkg/secrethub/client.go +++ b/pkg/secrethub/client.go @@ -1,9 +1,12 @@ package secrethub import ( + "os" + "github.com/secrethub/secrethub-go/internals/api" "github.com/secrethub/secrethub-go/internals/crypto" "github.com/secrethub/secrethub-go/internals/errio" + "github.com/secrethub/secrethub-go/pkg/secrethub/configdir" "github.com/secrethub/secrethub-go/pkg/secrethub/credentials" "github.com/secrethub/secrethub-go/pkg/secrethub/internals/http" ) @@ -14,14 +17,23 @@ const ( // ClientAdapter is an interface that can be used to consume the SecretHub client and is implemented by secrethub.Client. type ClientAdapter interface { + // AccessRules returns a service used to manage access rules. AccessRules() AccessRuleService + // Accounts returns a service used to manage SecretHub accounts. Accounts() AccountService + // Dirs returns a service used to manage directories. Dirs() DirService + // Me returns a service used to manage the current authenticated account. Me() MeService + // Orgs returns a service used to manage shared organization workspaces. Orgs() OrgService + // Repos returns a service used to manage repositories. Repos() RepoService + // Secrets returns a service used to manage secrets. Secrets() SecretService + // Services returns a service used to manage non-human service accounts. Services() ServiceService + // Users returns a service used to manage (human) user accounts. Users() UserService } @@ -47,7 +59,8 @@ type Client struct { // These are cached repoIndexKeys map[api.RepoPath]*crypto.SymmetricKey - appInfo *AppInfo + appInfo *AppInfo + ConfigDir *configdir.Dir } // AppInfo contains information about the application that is using the SecretHub client. @@ -81,16 +94,23 @@ func NewClient(with ...ClientOption) (*Client, error) { httpClient: http.NewClient(), repoIndexKeys: make(map[api.RepoPath]*crypto.SymmetricKey), } - for _, option := range with { - err := option(client) + err := client.with(with...) + if err != nil { + return nil, err + } + + // ConfigDir should be fully initialized before loading any default credentials. + if client.ConfigDir == nil { + configDir, err := configdir.Default() if err != nil { return nil, err } + client.ConfigDir = configDir } // Try to use default key credentials if none provided explicitly if client.decrypter == nil { - err := WithCredentials(credentials.UseKey(nil, nil))(client) + err := client.with(WithCredentials(credentials.UseKey(client.DefaultCredential()))) // nolint: staticcheck if err != nil { // TODO: log that default credential was not loaded. @@ -122,47 +142,70 @@ func Must(c *Client, err error) *Client { return c } -// AccessRules returns an AccessRuleService. +// AccessRules returns a service used to manage access rules. func (c *Client) AccessRules() AccessRuleService { return newAccessRuleService(c) } -// Accounts returns an AccountService. +// Accounts returns a service used to manage SecretHub accounts. func (c *Client) Accounts() AccountService { return newAccountService(c) } -// Dirs returns an DirService. +// Dirs returns a service used to manage directories. func (c *Client) Dirs() DirService { return newDirService(c) } -// Me returns a MeService. +// Me returns a service used to manage the current authenticated account. func (c *Client) Me() MeService { return newMeService(c) } -// Orgs returns an OrgService. +// Orgs returns a service used to manage shared organization workspaces. func (c *Client) Orgs() OrgService { return newOrgService(c) } -// Repos returns an RepoService. +// Repos returns a service used to manage repositories. func (c *Client) Repos() RepoService { return newRepoService(c) } -// Secrets returns an SecretService. +// Secrets returns a service used to manage secrets. func (c *Client) Secrets() SecretService { return newSecretService(c) } -// Services returns an ServiceService. +// Services returns a service used to manage non-human service accounts. func (c *Client) Services() ServiceService { return newServiceService(c) } -// Users returns an UserService. +// Users returns a service used to manage (human) user accounts. func (c *Client) Users() UserService { return newUserService(c) } + +// with applies ClientOptions to a Client. Should only be called during initialization. +func (c *Client) with(options ...ClientOption) error { + for _, o := range options { + err := o(c) + if err != nil { + return err + } + } + return nil +} + +// DefaultCredential returns a reader pointing to the configured credential, +// 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") + if envCredential != "" { + return credentials.FromString(envCredential) + } + + return c.ConfigDir.Credential() +} diff --git a/pkg/secrethub/client_options.go b/pkg/secrethub/client_options.go index 3351bc89..16a086ba 100644 --- a/pkg/secrethub/client_options.go +++ b/pkg/secrethub/client_options.go @@ -5,6 +5,7 @@ import ( "net/http" "time" + "github.com/secrethub/secrethub-go/pkg/secrethub/configdir" "github.com/secrethub/secrethub-go/pkg/secrethub/credentials" httpclient "github.com/secrethub/secrethub-go/pkg/secrethub/internals/http" ) @@ -47,6 +48,14 @@ func WithAppInfo(appInfo *AppInfo) ClientOption { } } +// WithConfigDir sets the configuration directory to use (among others) for sourcing the credential file from. +func WithConfigDir(configDir configdir.Dir) ClientOption { + return func(c *Client) error { + c.ConfigDir = &configDir + return nil + } +} + // WithCredentials sets the credential to be used for authenticating to the API and decrypting the account key. func WithCredentials(provider credentials.Provider) ClientOption { return func(c *Client) error { diff --git a/pkg/secrethub/configdir/dir.go b/pkg/secrethub/configdir/dir.go new file mode 100644 index 00000000..51b6106b --- /dev/null +++ b/pkg/secrethub/configdir/dir.go @@ -0,0 +1,106 @@ +// Package configdir provides simple functions to manage the SecretHub +// configuration directory. +package configdir + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "github.com/mitchellh/go-homedir" +) + +var ( + // ErrCredentialNotFound is returned when a credential file does not exist but CredentialFile.Read() is called. + ErrCredentialNotFound = errors.New("credential not found") +) + +// Dir represents the configuration directory located at some path +// on the file system. +type Dir struct { + path string +} + +// New a new Dir which represents a configuration directory at the given location. +func New(path string) Dir { + return Dir{ + path: path, + } +} + +// Default is the default way to get the location of the SecretHub +// configuration directory, sourcing it from the environment variable +// SECRETHUB_CONFIG_DIR or falling back to the ~/.secrethub directory. +func Default() (*Dir, error) { + envDir := os.Getenv("SECRETHUB_CONFIG_DIR") + if envDir != "" { + return &Dir{ + path: envDir, + }, nil + } + + homeDir, err := homedir.Dir() + if err != nil { + return &Dir{}, fmt.Errorf("cannot get home directory: %v", err) + } + return &Dir{ + path: filepath.Join(homeDir, ".secrethub"), + }, nil +} + +// Credential returns the file that contains the SecretHub API credential. +func (c Dir) Credential() *CredentialFile { + return &CredentialFile{ + path: filepath.Join(c.path, "credential"), + } +} + +// Path returns the path on the filesystem at which the config directory is located. +func (c Dir) Path() string { + return c.path +} + +func (c Dir) String() string { + return c.path +} + +// CredentialFile represents the file that contains the SecretHub API credential. +// By default, it's a file named "credential" in the configuration directory. +type CredentialFile struct { + path string +} + +// Path returns the path on the filesystem at which the credential file is located. +func (f *CredentialFile) Path() 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)) + if err != nil { + return err + } + return ioutil.WriteFile(f.path, data, os.FileMode(0600)) +} + +// Exists returns true when a file exists at the path this credential points to. +func (f *CredentialFile) Exists() bool { + if _, err := os.Stat(f.path); os.IsNotExist(err) { + return false + } + return true +} + +// Read reads from the filesystem and returns the contents of the credential file. +func (f *CredentialFile) Read() ([]byte, error) { + file, err := os.Open(f.path) + if os.IsNotExist(err) { + return nil, ErrCredentialNotFound + } else if err != nil { + return nil, err + } + return ioutil.ReadAll(file) +} diff --git a/pkg/secrethub/credentials/creators.go b/pkg/secrethub/credentials/creators.go index c86d5fb0..5c012caa 100644 --- a/pkg/secrethub/credentials/creators.go +++ b/pkg/secrethub/credentials/creators.go @@ -1,10 +1,6 @@ package credentials import ( - "errors" - "net/http" - - "github.com/secrethub/secrethub-go/internals/auth" "github.com/secrethub/secrethub-go/internals/aws" "github.com/secrethub/secrethub-go/internals/crypto" @@ -13,12 +9,14 @@ import ( // Creator is an interface is accepted by functions that need a new credential to be created. type Creator interface { - Create() (Verifier, Encrypter, map[string]string, error) -} - -// KeyCreator is used to create a new key-based credential. -type KeyCreator struct { - key *RSACredential + // Create creates the actual credential (e.g. by generating a key). + Create() error + // Verifier returns information that the server can use to verify a request authenticated with the credential. + Verifier() Verifier + // Encrypter returns a wrapper that is used to encrypt data, typically an account key. + Encrypter() Encrypter + // Metadata returns a set of metadata about the credential. The result can be empty if no metadata is provided. + Metadata() map[string]string } // CreateKey returns a Creator that creates a key based credential. @@ -29,29 +27,24 @@ func CreateKey() *KeyCreator { return &KeyCreator{} } +// KeyCreator is used to create a new key-based credential. +type KeyCreator struct { + Key +} + // Create generates a new key and stores it in the KeyCreator. -func (c *KeyCreator) Create() (Verifier, Encrypter, map[string]string, error) { +func (kc *KeyCreator) Create() error { key, err := GenerateRSACredential(crypto.RSAKeyLength) if err != nil { - return nil, nil, nil, err - } - c.key = key - return c.key, c.key, map[string]string{}, nil -} - -// Export the key of this credential to string format to save for later use. -// This can only be called after Create() is executed, for example by secrethub.UserService.Create([...]) -// or secrethub.ServiceService.Create([...]) -func (c *KeyCreator) Export() (string, error) { - if c.key == nil { - return "", errors.New("key has not yet been generated created. Use KeyCreator before calling Export()") + return err } - return EncodeCredential(c.key) + kc.key = key + return nil } -// Provide returns a credential that can be used for authentication and decryption. -func (c *KeyCreator) Provide(*http.Client) (auth.Authenticator, Decrypter, error) { - return c.key, c.key, nil +// Metadata returns a set of metadata associated with this credential. +func (kc *KeyCreator) Metadata() map[string]string { + return map[string]string{} } // CreateAWS returns a Creator that creates an AWS-based credential. @@ -61,19 +54,40 @@ func (c *KeyCreator) Provide(*http.Client) (auth.Authenticator, Decrypter, error // awsCfg can be used to optionally configure the used AWS client. For example to set the region. // The KMS key id and role are returned in the credentials metadata. func CreateAWS(kmsKeyID string, roleARN string, awsCfg ...*awssdk.Config) Creator { - return creatorFunc(func() (Verifier, Encrypter, map[string]string, error) { - creator, metadata, err := aws.NewCredentialCreator(kmsKeyID, roleARN, awsCfg...) - if err != nil { - return nil, nil, nil, err - } - return creator, creator, metadata, nil - }) + return &awsCreator{ + kmsKeyID: kmsKeyID, + roleARN: roleARN, + awsCfg: awsCfg, + } +} + +type awsCreator struct { + kmsKeyID string + roleARN string + awsCfg []*awssdk.Config + + credentialCreator *aws.CredentialCreator + metadata map[string]string } -// creatorFunc is a helper type that can transform any func() (Verifier, Encrypter, map[string]string, error) into a Creator. -type creatorFunc func() (Verifier, Encrypter, map[string]string, error) +func (ac *awsCreator) Create() error { + creator, metadata, err := aws.NewCredentialCreator(ac.kmsKeyID, ac.roleARN, ac.awsCfg...) + if err != nil { + return err + } + ac.credentialCreator = creator + ac.metadata = metadata + return nil +} + +func (ac *awsCreator) Verifier() Verifier { + return ac.credentialCreator +} + +func (ac *awsCreator) Encrypter() Encrypter { + return ac.credentialCreator +} -// Create is implemented to let creatorFunc implement the Creator interface. -func (f creatorFunc) Create() (Verifier, Encrypter, map[string]string, error) { - return f() +func (ac *awsCreator) Metadata() map[string]string { + return ac.metadata } diff --git a/pkg/secrethub/credentials/credentials.go b/pkg/secrethub/credentials/credentials.go index b4f0f099..c82e3d47 100644 --- a/pkg/secrethub/credentials/credentials.go +++ b/pkg/secrethub/credentials/credentials.go @@ -1,11 +1,12 @@ +// Package credentials provides utilities for managing SecretHub API credentials. package credentials import "github.com/secrethub/secrethub-go/internals/api" // Verifier exports verification bytes that can be used to verify signed data is processed by the owner of a signer. type Verifier interface { - // Verifier returns the data to be stored server side to verify an http request authenticated with this credential. - Verifier() ([]byte, error) + // Export returns the data to be stored server side to verify an http request authenticated with this credential. + Export() (verifierBytes []byte, fingerprint string, err error) // Type returns what type of credential this is. Type() api.CredentialType // AddProof adds the proof of this credential's possession to a CreateCredentialRequest. diff --git a/pkg/secrethub/credentials/encoding.go b/pkg/secrethub/credentials/encoding.go index 5a4a5f19..ef6e28c0 100644 --- a/pkg/secrethub/credentials/encoding.go +++ b/pkg/secrethub/credentials/encoding.go @@ -1,6 +1,7 @@ package credentials import ( + "bytes" "crypto/x509" "encoding/base64" "encoding/json" @@ -40,50 +41,17 @@ var ( // We'll migrate away from using it and use smaller interfaces instead. // See Verifier, Decrypter and Encrypter for the smaller interfaces. type EncodableCredential interface { - // Export exports the credential in a format that can be decoded by its Decoder. - Export() []byte + // Encode the credential to a format that can be decoded by its Decoder. + Encode() []byte // Decoder returns a Decoder that can decode an exported key back into a Credential. Decoder() Decoder } -// UnpackRSACredential is a shorthand function to decode a credential string and optionally -// decrypt it with a passphrase. When an encrypted credential is given, the passphrase -// cannot be empty. -// -// Note that when you want to customize the process of parsing and decoding/decrypting -// a credential (e.g. to prompt only for a passphrase when the credential is encrypted), -// it is recommended you use a CredentialParser instead (e.g. defaultParser). -func UnpackRSACredential(credential string, passphrase string) (*RSACredential, error) { - encoded, err := defaultParser.parse(credential) - if err != nil { - return nil, errio.Error(err) - } - - if encoded.IsEncrypted() { - if passphrase == "" { - return nil, ErrEmptyCredentialPassphrase - } - - key, err := NewPassBasedKey([]byte(passphrase)) - if err != nil { - return nil, err - } - - credential, err := encoded.DecodeEncrypted(key) - if crypto.IsWrongKey(err) { - return nil, ErrCannotDecryptCredential - } - return credential, err - } - - return encoded.Decode() -} - // encodedCredential is an intermediary format for encoding and decoding credentials. type encodedCredential struct { // Raw is the raw credential string. // Populated when you Parse a credential. - Raw string + Raw []byte // Header is the decoded first part of the credential string. Header map[string]interface{} // RawHeader is the first part of the credential string, encoded as json. @@ -127,14 +95,14 @@ func (c encodedCredential) IsEncrypted() bool { } // EncodeCredential encodes a Credential as a one line string that can be transferred. -func EncodeCredential(credential EncodableCredential) (string, error) { +func EncodeCredential(credential EncodableCredential) ([]byte, error) { cred := newEncodedCredential(credential) - return encodeCredentialPartsToString(cred.Header, cred.Payload) + return encodeCredentialParts(cred.Header, cred.Payload) } // EncodeEncryptedCredential encrypts and encodes a Credential as a one line string token that can be transferred. -func EncodeEncryptedCredential(credential EncodableCredential, key PassBasedKey) (string, error) { +func EncodeEncryptedCredential(credential EncodableCredential, key PassBasedKey) ([]byte, error) { cred := newEncodedCredential(credential) // Set the `enc` header so it can be used to decrypt later. @@ -142,14 +110,14 @@ func EncodeEncryptedCredential(credential EncodableCredential, key PassBasedKey) payload, additionalheaders, err := key.Encrypt(cred.Payload) if err != nil { - return "", errio.Error(err) + return nil, errio.Error(err) } for key, value := range additionalheaders { cred.Header[key] = value } - return encodeCredentialPartsToString(cred.Header, payload) + return encodeCredentialParts(cred.Header, payload) } // newEncodedCredential creates exports and encodes a credential in the payload. @@ -160,26 +128,26 @@ func newEncodedCredential(credential EncodableCredential) *encodedCredential { Header: map[string]interface{}{ "type": decoder.Name(), }, - Payload: credential.Export(), + Payload: credential.Encode(), Decoder: decoder, } } -// encodeCredentialPartsToString encodes an header and payload in a format string: header.payload -func encodeCredentialPartsToString(header map[string]interface{}, payload []byte) (string, error) { +// encodeCredentialParts encodes an header and payload in `header.payload` format. +func encodeCredentialParts(header map[string]interface{}, payload []byte) ([]byte, error) { if len(header) == 0 { - return "", ErrEmptyCredentialHeader + return nil, ErrEmptyCredentialHeader } parts := make([]string, 2) headerBytes, err := json.Marshal(header) if err != nil { - return "", ErrInvalidCredential.Errorf("cannot encode header as json: %s", err) + return nil, ErrInvalidCredential.Errorf("cannot encode header as json: %s", err) } parts[0] = defaultEncoding.EncodeToString(headerBytes) parts[1] = defaultEncoding.EncodeToString(payload) - return strings.Join(parts, "."), nil + return []byte(strings.Join(parts, ".")), nil } // parser parses a credential string with support @@ -201,20 +169,21 @@ func newParser(decoders []Decoder) parser { } // parse parses a credential string. -func (p parser) parse(raw string) (*encodedCredential, error) { - parts := strings.Split(raw, ".") +func (p parser) parse(raw []byte) (*encodedCredential, error) { + parts := bytes.Split(raw, []byte(".")) if len(parts) != 2 { return nil, ErrInvalidNumberOfCredentialSegments(len(parts)) } cred := &encodedCredential{ - Raw: raw, - Header: make(map[string]interface{}), + Raw: raw, + Header: make(map[string]interface{}), + RawHeader: make([]byte, defaultEncoding.DecodedLen(len(parts[0]))), + Payload: make([]byte, defaultEncoding.DecodedLen(len(parts[1]))), } // Decode the header - var err error - cred.RawHeader, err = defaultEncoding.DecodeString(parts[0]) + _, err := defaultEncoding.Decode(cred.RawHeader, parts[0]) if err != nil { return nil, ErrCannotDecodeCredentialHeader(err) } @@ -235,7 +204,7 @@ func (p parser) parse(raw string) (*encodedCredential, error) { return nil, ErrUnsupportedCredentialType(payloadType) } - cred.Payload, err = defaultEncoding.DecodeString(parts[1]) + _, err = defaultEncoding.Decode(cred.Payload, parts[1]) if err != nil { return nil, ErrCannotDecodeCredentialPayload(err) } diff --git a/pkg/secrethub/credentials/encoding_test.go b/pkg/secrethub/credentials/encoding_test.go index a0dad8f7..909b99e5 100644 --- a/pkg/secrethub/credentials/encoding_test.go +++ b/pkg/secrethub/credentials/encoding_test.go @@ -51,7 +51,7 @@ func TestRSACredential(t *testing.T) { assert.OK(t, err) t.Run("encoding", func(t *testing.T) { - exported := credential.Export() + exported := credential.Encode() decoder := credential.Decoder() actual, err := decoder.Decode(exported) @@ -87,7 +87,7 @@ func TestParser(t *testing.T) { credential, err := GenerateRSACredential(1024) assert.OK(t, err) - payload := credential.Export() + payload := credential.Encode() header := map[string]interface{}{ "type": credential.Decoder().Name(), @@ -126,7 +126,7 @@ func TestParser(t *testing.T) { "valid_rsa": { raw: raw, expected: &encodedCredential{ - Raw: raw, + Raw: []byte(raw), Header: header, RawHeader: headerBytes, Payload: payload, @@ -138,7 +138,7 @@ func TestParser(t *testing.T) { "valid_rsa_encrypted": { raw: rawEncrypted, expected: &encodedCredential{ - Raw: rawEncrypted, + Raw: []byte(rawEncrypted), Header: headerEncrypted, RawHeader: headerEncryptedBytes, Payload: payload, @@ -209,7 +209,7 @@ func TestParser(t *testing.T) { for name, tc := range cases { t.Run(name, func(t *testing.T) { // Act - actual, err := parser.parse(tc.raw) + actual, err := parser.parse([]byte(tc.raw)) // Assert assert.Equal(t, err, tc.err) @@ -268,19 +268,19 @@ func TestEncodeEncryptedCredential(t *testing.T) { assert.Equal(t, cred, decoded) } -func TestEncodeCredentialPartsToString(t *testing.T) { +func TestEncodeCredentialParts(t *testing.T) { // Arrange cases := map[string]struct { header map[string]interface{} payload []byte - expected string + expected []byte err error }{ "success": { header: exampleHeader, payload: []byte(foo), - expected: fmt.Sprintf("%s.%s", exampleHeaderEncoded, fooEncoded), + expected: []byte(fmt.Sprintf("%s.%s", exampleHeaderEncoded, fooEncoded)), }, "nil_header": { header: nil, @@ -297,7 +297,7 @@ func TestEncodeCredentialPartsToString(t *testing.T) { for name, tc := range cases { t.Run(name, func(t *testing.T) { // Act - actual, err := encodeCredentialPartsToString(tc.header, tc.payload) + actual, err := encodeCredentialParts(tc.header, tc.payload) assert.Equal(t, err, tc.err) // Assert diff --git a/pkg/secrethub/credentials/key.go b/pkg/secrethub/credentials/key.go new file mode 100644 index 00000000..211c2767 --- /dev/null +++ b/pkg/secrethub/credentials/key.go @@ -0,0 +1,112 @@ +package credentials + +import ( + "errors" + + "github.com/secrethub/secrethub-go/internals/auth" + "github.com/secrethub/secrethub-go/internals/crypto" + "github.com/secrethub/secrethub-go/pkg/secrethub/internals/http" +) + +// Key is a credential that uses a local key for all its operations. +type Key struct { + key *RSACredential + exportPassphrase Reader +} + +// Verifier returns a Verifier that can be used for creating a new credential from this Key. +func (k Key) Verifier() Verifier { + return k.key +} + +// Encrypter returns a Encrypter that can be used to encrypt data with this Key. +func (k Key) Encrypter() Encrypter { + return k.key +} + +// Provide implements the Provider interface for a Key. +func (k Key) Provide(httpClient *http.Client) (auth.Authenticator, Decrypter, error) { + return k.key, k.key, nil +} + +// Passphrase returns a new Key that uses the provided passphraseReader to obtain a passphrase that is used for +// encryption when Export() is called. +func (k Key) Passphrase(passphraseReader Reader) Key { + k.exportPassphrase = passphraseReader + return k +} + +// Export the key of this credential to string format to save for later use. +// If a passphrase was set with Passphrase(), this passphrase is used for encrypting the key. +func (k Key) Export() ([]byte, error) { + if k.key == nil { + return nil, errors.New("key has not yet been generated created. Use KeyCreator before calling Export()") + } + if k.exportPassphrase != nil { + passphrase, err := k.exportPassphrase.Read() + if err != nil { + return nil, err + } + passBasedKey, err := NewPassBasedKey(passphrase) + if err != nil { + return nil, err + } + return EncodeEncryptedCredential(k.key, passBasedKey) + } + return EncodeCredential(k.key) +} + +// ImportKey returns a Key by loading it from the provided credentialReader. +// If the key is encrypted with a passphrase, passphraseReader should be provided. This is used to read a passphrase +// from that is used for decryption. If the passphrase is incorrect, a new passphrase will be read up to 3 times. +func ImportKey(credentialReader, passphraseReader Reader) (Key, error) { + bytes, err := credentialReader.Read() + if err != nil { + return Key{}, err + } + encoded, err := defaultParser.parse(bytes) + if err != nil { + return Key{}, err + } + if encoded.IsEncrypted() { + if passphraseReader == nil { + return Key{}, errors.New("need passphrase") + } + + // Try up to three times to get the correct passphrase. + for i := 0; i < 3; i++ { + passphrase, err := passphraseReader.Read() + if err != nil { + return Key{}, err + } + if len(passphrase) == 0 { + continue + } + + credential, err := decryptKey(passphrase, encoded) + if crypto.IsWrongKey(err) { + continue + } else if err != nil { + return Key{}, err + } + + return Key{key: credential}, nil + } + + return Key{}, ErrCannotDecryptCredential + } + credential, err := encoded.Decode() + if err != nil { + return Key{}, err + } + + return Key{key: credential}, nil +} + +func decryptKey(passphrase []byte, encoded *encodedCredential) (*RSACredential, error) { + key, err := NewPassBasedKey(passphrase) + if err != nil { + return nil, err + } + return encoded.DecodeEncrypted(key) +} diff --git a/pkg/secrethub/credentials/providers.go b/pkg/secrethub/credentials/providers.go index 4404337c..b091e15c 100644 --- a/pkg/secrethub/credentials/providers.go +++ b/pkg/secrethub/credentials/providers.go @@ -1,15 +1,10 @@ package credentials import ( - "errors" - "io" - "io/ioutil" - awssdk "github.com/aws/aws-sdk-go/aws" "github.com/secrethub/secrethub-go/internals/auth" "github.com/secrethub/secrethub-go/internals/aws" - "github.com/secrethub/secrethub-go/internals/crypto" "github.com/secrethub/secrethub-go/pkg/secrethub/credentials/sessions" "github.com/secrethub/secrethub-go/pkg/secrethub/internals/http" ) @@ -39,59 +34,38 @@ func UseAWS(awsCfg ...*awssdk.Config) Provider { } // UseKey returns a Provider that reads a key credential from credentialReader. -// If the key credential is encrypted, a passphrase is read from passReader and used for decryption, -// The passReader argument can be set to nil if the credential is not encrypted. -// If credentialReader argument is set to nil, the following default locations are searched for a credential: -// 1. The SECRETHUB_CREDENTIAL environment variable. -// 2. The credential file placed in the directory given by the SECRETHUB_CONFIG_DIR environment variable. -// 3. The credential file found in /.secrethub/credential. +// If the key credential is encrypted, a passphrase must be set by calling Passphrase on the returned KeyProvider, // // Usage: -// credentials.UseKey(credentials.FromString(""), nil) -// credentials.UseKey(credentials.FromFile("/path/to/credential"), credentials.FromString("passphrase")) -func UseKey(credentialReader io.Reader, passReader io.Reader) Provider { - return providerFunc(func(_ *http.Client) (auth.Authenticator, Decrypter, error) { - // This function can be cleaned up a lot. It is mainly for demonstrating the overall idea. - if credentialReader == nil { - credentialReader = credentialFromDefault() - } +// credentials.UseKey(credentials.FromString("")) +// credentials.UseKey(credentials.FromFile("/path/to/credential")).Passphrase(credentials.FromString("passphrase")) +func UseKey(credentialReader Reader) KeyProvider { + return KeyProvider{ + credentialReader: credentialReader, + } +} - bytes, err := ioutil.ReadAll(credentialReader) - if err != nil { - return nil, nil, err - } - encoded, err := defaultParser.parse(string(bytes)) - if err != nil { - return nil, nil, err - } - if encoded.IsEncrypted() { - if passReader == nil { - return nil, nil, errors.New("need passphrase") - } - passphrase, err := ioutil.ReadAll(passReader) - if err != nil { - return nil, nil, err - } - key, err := NewPassBasedKey(passphrase) - if err != nil { - return nil, nil, err - } +// KeyProvider is a Provider that reads a key from a Reader. +// If the key is encrypted with a passphrase, Passphrase() should be called on the KeyProvider to set the Reader that +// provides the passphrase that can be used to decrypt the key. +type KeyProvider struct { + credentialReader Reader + passphraseReader Reader +} - credential, err := encoded.DecodeEncrypted(key) - if crypto.IsWrongKey(err) { - return nil, nil, ErrCannotDecryptCredential - } else if err != nil { - return nil, nil, err - } - return credential, credential, nil - } - credential, err := encoded.Decode() - if err != nil { - return nil, nil, err - } +// Passphrase returns a new Provider that uses the passphraseReader to read a passphrase if the read key is encrypted. +func (k KeyProvider) Passphrase(passphraseReader Reader) Provider { + k.passphraseReader = passphraseReader + return k +} - return credential, credential, nil - }) +// Provide implements the Provider interface for a KeyProvider. +func (k KeyProvider) Provide(httpClient *http.Client) (auth.Authenticator, Decrypter, error) { + key, err := ImportKey(k.credentialReader, k.passphraseReader) + if err != nil { + return nil, nil, err + } + return key.Provide(httpClient) } // providerFunc is a helper type to let any func(*http.Client) (UsableCredential, error) implement the Provider interface. diff --git a/pkg/secrethub/credentials/readers.go b/pkg/secrethub/credentials/readers.go index 024f0df8..67fe88bd 100644 --- a/pkg/secrethub/credentials/readers.go +++ b/pkg/secrethub/credentials/readers.go @@ -1,83 +1,52 @@ package credentials import ( - "bytes" - "io" + "io/ioutil" "os" - "path/filepath" - "strings" - - "github.com/mitchellh/go-homedir" -) - -// Errors -var ( - ErrCannotFindHomeDir = errCredentials.Code("cannot_find_home_dir").ErrorPref( - "cannot find your home directory: %s", - ) ) -// FromFile returns an io.Reader that reads the contents from a file. -// This can be used to read a credential or passphrase from a file. -func FromFile(path string) io.Reader { - return readerFunc(func() (io.Reader, error) { - return os.Open(path) - }) +// Reader helps with reading bytes from a configured source. +type Reader interface { + // Read reads from the reader and returns the resulting bytes. + Read() ([]byte, error) } -// FromEnv returns an io.Reader that reads the content of an environment variable. -// This can be used to read a credential or passphrase from a file. -func FromEnv(key string) io.Reader { - return readerFunc(func() (io.Reader, error) { - return strings.NewReader(os.Getenv(key)), nil +// 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 ioutil.ReadFile(path) }) } -// FromBytes returns an io.Reader that reads the provided bytes. -// This can be used to read a credential or passphrase from a byte slice. -func FromBytes(raw []byte) io.Reader { - return readerFunc(func() (io.Reader, error) { - return bytes.NewReader(raw), nil +// 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 []byte(os.Getenv(key)), nil }) } -// FromString returns an io.Reader that reads the provided string. -// This can be used to read a credential or passphrase from a string. -func FromString(raw string) io.Reader { - return readerFunc(func() (io.Reader, error) { - return strings.NewReader(raw), nil +// FromBytes returns a reader that simply returns the given bytes +// when Read() is called. +func FromBytes(raw []byte) Reader { + return readerFunc(func() ([]byte, error) { + return raw, nil }) } -// credentialFromDefault returns an io.Reader that tries to read a credential from any of the default locations. -func credentialFromDefault() io.Reader { - return readerFunc(func() (io.Reader, error) { - envCredential := os.Getenv("SECRETHUB_CREDENTIAL") - if envCredential != "" { - return strings.NewReader(envCredential), nil - } - - configDir := os.Getenv("SECRETHUB_CONFIG_DIR") - if configDir == "" { - home, err := homedir.Dir() - if err != nil { - return nil, ErrCannotFindHomeDir(err) - } - configDir = filepath.Join(home, ".secrethub") - } - - return os.Open(filepath.Join(configDir, ".credential")) +// FromString returns a reader that simply returns the given string as +// a byte slice when Read() is called. +func FromString(raw string) Reader { + return readerFunc(func() ([]byte, error) { + return []byte(raw), nil }) } -// readerFunc is a helper function to create a io.Reader from any func() (io.Reader, error). -type readerFunc func() (io.Reader, error) +// readerFunc is a helper function to create a reader from any func() ([]byte, error). +type readerFunc func() ([]byte, error) -// Read implements Read() on readerFunc to implement the io.Reader interface. -func (f readerFunc) Read(p []byte) (n int, err error) { - reader, err := f() - if err != nil { - return 0, err - } - return reader.Read(p) +// Read implements the Reader interface. +func (f readerFunc) Read() ([]byte, error) { + return f() } diff --git a/pkg/secrethub/credentials/rsa.go b/pkg/secrethub/credentials/rsa.go index 3524c18c..5247f31b 100644 --- a/pkg/secrethub/credentials/rsa.go +++ b/pkg/secrethub/credentials/rsa.go @@ -29,17 +29,22 @@ func GenerateRSACredential(keyLength int) (*RSACredential, error) { } // Fingerprint returns the key identifier by which the server can identify the credential. -func (c RSACredential) Fingerprint() (string, error) { - verifier, err := c.Verifier() +func (c RSACredential) Export() ([]byte, string, error) { + verifier, err := c.RSAPrivateKey.Public().Encode() if err != nil { - return "", err + return nil, "", err } - return api.GetFingerprint(c.Type(), verifier) + fingerprint, err := api.GetFingerprint(c.Type(), verifier) + if err != nil { + return nil, "", err + } + return verifier, fingerprint, nil } // ID returns a string by which the credential can be identified. func (c RSACredential) ID() (string, error) { - return c.Fingerprint() + _, fingerprint, err := c.Export() + return fingerprint, err } // Sign provides proof the given bytes are processed by the owner of the credential. @@ -52,11 +57,6 @@ func (c RSACredential) SignMethod() string { return "PKCS1v15" } -// Verifier returns the public key to be stored server side to verify an http request authenticated with this credential. -func (c RSACredential) Verifier() ([]byte, error) { - return c.RSAPrivateKey.Public().Export() -} - // Decoder returns the Decoder for the rsa private key. func (c RSACredential) Decoder() Decoder { return rsaPrivateKeyDecoder{} diff --git a/pkg/secrethub/main_test.go b/pkg/secrethub/main_test.go index 1d6e5754..fb1a7bf6 100644 --- a/pkg/secrethub/main_test.go +++ b/pkg/secrethub/main_test.go @@ -23,17 +23,12 @@ func init() { panic(err) } - cred1PublicKey, err = cred1.Public().Export() + cred1PublicKey, err = cred1.Public().Encode() if err != nil { panic(err) } - cred1Fingerprint, err = cred1.Fingerprint() - if err != nil { - panic(err) - } - - cred1Verifier, err = cred1.Verifier() + cred1Verifier, cred1Fingerprint, err = cred1.Export() if err != nil { panic(err) } diff --git a/pkg/secrethub/service.go b/pkg/secrethub/service.go index 01a97a2f..71869959 100644 --- a/pkg/secrethub/service.go +++ b/pkg/secrethub/service.go @@ -40,7 +40,7 @@ func (s serviceService) Create(path string, description string, credentialCreato return nil, errio.Error(err) } - verifier, encrypter, metadata, err := credentialCreator.Create() + err = credentialCreator.Create() if err != nil { return nil, err } @@ -50,12 +50,12 @@ func (s serviceService) Create(path string, description string, credentialCreato return nil, errio.Error(err) } - credentialRequest, err := s.client.createCredentialRequest(verifier, metadata) + credentialRequest, err := s.client.createCredentialRequest(credentialCreator.Verifier(), credentialCreator.Metadata()) if err != nil { return nil, errio.Error(err) } - accountKeyRequest, err := s.client.createAccountKeyRequest(encrypter, accountKey) + accountKeyRequest, err := s.client.createAccountKeyRequest(credentialCreator.Encrypter(), accountKey) if err != nil { return nil, errio.Error(err) } diff --git a/pkg/secrethub/user.go b/pkg/secrethub/user.go index a9274582..5f271bc5 100644 --- a/pkg/secrethub/user.go +++ b/pkg/secrethub/user.go @@ -49,7 +49,7 @@ func (s userService) Create(username, email, fullName string, credentials creden return nil, errio.Error(err) } - verifier, encrypter, metadata, err := credentials.Create() + err = credentials.Create() if err != nil { return nil, err } @@ -59,7 +59,7 @@ func (s userService) Create(username, email, fullName string, credentials creden return nil, errio.Error(err) } - return s.create(username, email, fullName, accountKey, verifier, encrypter, metadata, credentials) + return s.create(username, email, fullName, accountKey, credentials.Verifier(), credentials.Encrypter(), credentials.Metadata(), credentials) } func (s userService) create(username, email, fullName string, accountKey crypto.RSAPrivateKey, verifier credentials.Verifier, encrypter credentials.Encrypter, metadata map[string]string, credentials credentials.Provider) (*api.User, error) { diff --git a/pkg/secrethub/user_test.go b/pkg/secrethub/user_test.go index 00d98342..aaf84826 100644 --- a/pkg/secrethub/user_test.go +++ b/pkg/secrethub/user_test.go @@ -71,7 +71,7 @@ func TestSignup(t *testing.T) { accountKey, err := crypto.GenerateRSAPrivateKey(512) assert.OK(t, err) - publicAccountKey, err := accountKey.Public().Export() + publicAccountKey, err := accountKey.Public().Encode() assert.OK(t, err) router.Post(fmt.Sprintf("/me/credentials/%s/key", cred1Fingerprint), func(w http.ResponseWriter, r *http.Request) {