From 3d9ef03a3351ceda485b38448e84312338e13a8f Mon Sep 17 00:00:00 2001 From: Jacky Wang Date: Mon, 6 Jul 2020 17:14:26 -0700 Subject: [PATCH] [node.sh] added kmsProvider.go to provide kms client --- cmd/harmony/blsloader/kmsProvider.go | 216 ++++++++++++++++++ cmd/harmony/blsloader/{const.go => params.go} | 6 + .../{passprovider.go => passProvider.go} | 0 3 files changed, 222 insertions(+) create mode 100644 cmd/harmony/blsloader/kmsProvider.go rename cmd/harmony/blsloader/{const.go => params.go} (60%) rename cmd/harmony/blsloader/{passprovider.go => passProvider.go} (100%) diff --git a/cmd/harmony/blsloader/kmsProvider.go b/cmd/harmony/blsloader/kmsProvider.go new file mode 100644 index 000000000..2ef733628 --- /dev/null +++ b/cmd/harmony/blsloader/kmsProvider.go @@ -0,0 +1,216 @@ +package blsloader + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "sync" + "syscall" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/kms" + "github.com/pkg/errors" + "golang.org/x/crypto/ssh/terminal" +) + +type AwsConfig struct { + AccessKey string `json:"aws-access-key-id"` + SecretKey string `json:"aws-secret-access-key"` + Region string `json:"aws-region"` + Token string `json:"aws-token,omitempty"` +} + +func (cfg *AwsConfig) toAws() *aws.Config { + cred := credentials.NewStaticCredentials(cfg.AccessKey, cfg.SecretKey, cfg.Token) + return &aws.Config{ + Region: aws.String(cfg.Region), + Credentials: cred, + } +} + +// kmsClientProvider provides the kms client. Implemented by +// baseKMSProvider - abstract implementation +// sharedKMSProvider - provide the client with default .aws folder +// fileKMSProvider - provide the aws config with a json file +// promptKMSProvider - provide the config field from prompt with time out +type kmsClientProvider interface { + // getKMSClient returns the KMSClient of the kmsClientProvider with lazy loading. + getKMSClient() (*kms.KMS, error) + + // getAWSConfig returns the AwsConfig for different implementations + getAWSConfig() (*AwsConfig, error) +} + +// baseKMSProvider provide the kms client with singleton initialization through +// function getConfig for aws credential and regions loading. +type baseKMSProvider struct { + client *kms.KMS + err error + once sync.Once +} + +func (provider *baseKMSProvider) getKMSClient() (*kms.KMS, error) { + provider.once.Do(func() { + cfg, err := provider.getAWSConfig() + if err != nil { + provider.err = err + return + } + provider.client, provider.err = kmsClientWithConfig(cfg) + }) + if provider.err != nil { + return nil, provider.err + } + return provider.client, nil +} + +func (provider *baseKMSProvider) getAWSConfig() (*AwsConfig, error) { + return nil, errors.New("not implemented") +} + +// sharedKMSProvider provide the kms session with the default aws config +// locates in directory $HOME/.aws/config +type sharedKMSProvider struct { + baseKMSProvider +} + +func newSharedKMSProvider() *sharedKMSProvider { + return &sharedKMSProvider{ + baseKMSProvider{}, + } +} + +func (provider *sharedKMSProvider) getAWSConfig() (*AwsConfig, error) { + return nil, nil +} + +// fileKMSProvider provide the kms session from a file with json data of structure +// AwsConfig +type fileKMSProvider struct { + baseKMSProvider + + file string +} + +func newFileKMSProvider(file string) *fileKMSProvider { + return &fileKMSProvider{ + baseKMSProvider: baseKMSProvider{}, + file: file, + } +} + +func (provider *fileKMSProvider) getAWSConfig() (*AwsConfig, error) { + b, err := ioutil.ReadFile(provider.file) + if err != nil { + return nil, err + } + var cfg *AwsConfig + if err := json.Unmarshal(b, cfg); err != nil { + return nil, err + } + return cfg, nil +} + +// promptKMSProvider provide a user interactive console for AWS config. +// Three fields are asked: +// 1. AccessKey 2. SecretKey 3. Region +// Each field is asked with a timeout mechanism. +type promptKMSProvider struct { + baseKMSProvider + + timeout time.Duration +} + +func newPromptKMSProvider(timeout time.Duration) *promptKMSProvider { + if timeout == 0 { + timeout = defPromptTimeout + } + return &promptKMSProvider{ + baseKMSProvider: baseKMSProvider{}, + timeout: timeout, + } +} + +func (provider *promptKMSProvider) getAWSConfig() (*AwsConfig, error) { + fmt.Println("Please provide AWS configurations for KMS encoded BLS keys:") + accessKey, err := provider.prompt(" AccessKey:") + if err != nil { + return nil, fmt.Errorf("cannot get aws access key: %v", err) + } + secretKey, err := provider.prompt(" SecretKey:") + if err != nil { + return nil, fmt.Errorf("cannot get aws secret key: %v", err) + } + region, err := provider.prompt("Region:") + if err != nil { + return nil, fmt.Errorf("cannot get aws region: %v", err) + } + return &AwsConfig{ + AccessKey: accessKey, + SecretKey: secretKey, + Region: region, + Token: "", + }, nil +} + +// prompt prompt the user to input a string for a certain field with timeout. +func (provider *promptKMSProvider) prompt(hint string) (string, error) { + var ( + res string + err error + + finished = make(chan struct{}) + timedOut = time.After(provider.timeout) + ) + + go func() { + res, err = provider.threadedPrompt(hint) + close(finished) + }() + + for { + select { + case <-finished: + return res, err + case <-timedOut: + return "", errors.New("timed out") + } + } +} + +func (provider *promptKMSProvider) threadedPrompt(hint string) (string, error) { + fmt.Print(hint) + b, err := terminal.ReadPassword(syscall.Stdin) + if err != nil { + return "", err + } + return string(b), nil +} + +func kmsClientWithConfig(config *AwsConfig) (*kms.KMS, error) { + if config == nil { + return getSharedKMSClient() + } + return getKMSClientFromConfig(config) +} + +func getSharedKMSClient() (*kms.KMS, error) { + sess, err := session.NewSessionWithOptions(session.Options{ + SharedConfigState: session.SharedConfigEnable, + }) + if err != nil { + return nil, errors.Wrapf(err, "failed to create aws session") + } + return kms.New(sess), err +} + +func getKMSClientFromConfig(config *AwsConfig) (*kms.KMS, error) { + sess, err := session.NewSession(config.toAws()) + if err != nil { + return nil, err + } + return kms.New(sess), nil +} diff --git a/cmd/harmony/blsloader/const.go b/cmd/harmony/blsloader/params.go similarity index 60% rename from cmd/harmony/blsloader/const.go rename to cmd/harmony/blsloader/params.go index 1fd0699bc..4063e3b67 100644 --- a/cmd/harmony/blsloader/const.go +++ b/cmd/harmony/blsloader/params.go @@ -1,7 +1,13 @@ package blsloader +import "time" + const ( passExt = ".pass" basicKeyExt = ".key" kmsKeyExt = ".bls" ) + +const ( + defPromptTimeout = 1 * time.Second +) diff --git a/cmd/harmony/blsloader/passprovider.go b/cmd/harmony/blsloader/passProvider.go similarity index 100% rename from cmd/harmony/blsloader/passprovider.go rename to cmd/harmony/blsloader/passProvider.go