diff --git a/cmd/harmony/blsloader/decrypter.go b/cmd/harmony/blsloader/decrypter.go deleted file mode 100644 index 70b37c977..000000000 --- a/cmd/harmony/blsloader/decrypter.go +++ /dev/null @@ -1,8 +0,0 @@ -package blsloader - -import bls_core "github.com/harmony-one/bls/ffi/go/bls" - -type decrypter interface { - extension() string - decrypt(keyFile string) (*bls_core.SecretKey, error) -} diff --git a/cmd/harmony/blsloader/helper.go b/cmd/harmony/blsloader/helper.go index 35e553325..e63ed24de 100644 --- a/cmd/harmony/blsloader/helper.go +++ b/cmd/harmony/blsloader/helper.go @@ -1,7 +1,6 @@ package blsloader import ( - "errors" "fmt" "os" "path/filepath" @@ -10,201 +9,84 @@ import ( "github.com/harmony-one/harmony/multibls" ) -// loadHelper defines the interface to help load bls keys +// loadHelper defines the helper interface to load bls keys. Implemented by +// multiKeyLoader - load key files with a slice of target key files +// blsDirLoader - load key files from a directory type loadHelper interface { loadKeys() (multibls.PrivateKeys, error) } -// basicSingleBlsLoader loads a single bls key file with passphrase -type basicSingleBlsLoader struct { - blsKeyFile string +// multiKeyLoader load keys from multiple bls key files +type multiKeyLoader struct { + keyFiles []string + decrypters map[string]keyDecrypter - passDecrypterConfig + loadedSecrets []*bls_core.SecretKey } -// loadKeys load bls keys from a single bls file -func (loader *basicSingleBlsLoader) loadKeys() (multibls.PrivateKeys, error) { - providers, err := loader.getPassProviders() - if err != nil { - return multibls.PrivateKeys{}, err +func newMultiKeyLoader(keyFiles []string, decrypters []keyDecrypter) (*multiKeyLoader, error) { + dm := make(map[string]keyDecrypter) + for _, decrypter := range decrypters { + dm[decrypter.extension()] = decrypter } - secretKey, err := loadBasicKey(loader.blsKeyFile, providers) - if err != nil { - return multibls.PrivateKeys{}, err - } - return multibls.GetPrivateKeys(secretKey), nil -} - -func (loader *basicSingleBlsLoader) getPassProviders() ([]passProvider, error) { - switch loader.passSrcType { - case PassSrcFile: - return []passProvider{loader.getFilePassProvider()}, nil - case PassSrcPrompt: - return []passProvider{loader.getPromptPassProvider()}, nil - case PassSrcAuto: - return []passProvider{ - loader.getFilePassProvider(), - loader.getPromptPassProvider(), - }, nil - default: - return nil, errors.New("unknown passphrase source type") - } -} - -func (loader *basicSingleBlsLoader) getFilePassProvider() passProvider { - if stringIsSet(loader.passFile) { - return newFilePassProvider(*loader.passFile) - } - passFile := keyFileToPassFileFull(loader.blsKeyFile) - return newFilePassProvider(passFile) -} - -func (loader *basicSingleBlsLoader) getPromptPassProvider() passProvider { - provider := newPromptPassProvider() - if loader.persistPassphrase { - provider.setPersist(filepath.Dir(loader.blsKeyFile)) + for _, keyFile := range keyFiles { + ext := filepath.Ext(keyFile) + if _, supported := dm[ext]; !supported { + return nil, fmt.Errorf("unsupported key extension: %v", ext) + } } - return provider -} - -// kmsSingleBlsLoader loads a single kms bls key -type kmsSingleBlsLoader struct { - blsKeyFile string - - kmsProviderConfig + return &multiKeyLoader{ + keyFiles: keyFiles, + decrypters: dm, + loadedSecrets: make([]*bls_core.SecretKey, 0, len(keyFiles)), + }, nil } -func (loader *kmsSingleBlsLoader) loadKeys() (multibls.PrivateKeys, error) { - provider, err := loader.getKmsClientProvider() - if err != nil { - return multibls.PrivateKeys{}, err - } - secretKey, err := loadKmsKeyFromFile(loader.blsKeyFile, provider) - if err != nil { - return multibls.PrivateKeys{}, err +func (loader *multiKeyLoader) loadKeys() (multibls.PrivateKeys, error) { + for _, keyFile := range loader.keyFiles { + decrypter := loader.decrypters[filepath.Ext(keyFile)] + secret, err := decrypter.decryptFile(keyFile) + if err != nil { + return multibls.PrivateKeys{}, err + } + loader.loadedSecrets = append(loader.loadedSecrets, secret) } - return multibls.GetPrivateKeys(secretKey), nil -} - -func (loader *kmsSingleBlsLoader) getKmsClientProvider() (kmsProvider, error) { - return newLazyKmsProvider(loader.kmsProviderConfig) + return multibls.GetPrivateKeys(loader.loadedSecrets...), nil } -// blsDirLoader is the helper for loading bls keys in a directory type blsDirLoader struct { - // input fields - dirPath string - passDecrypterConfig - kmsProviderConfig - - // providers in process - pps []passProvider - kcp kmsProvider - // result field - secretKeys []*bls_core.SecretKey -} - -func (loader *blsDirLoader) loadKeys() (multibls.PrivateKeys, error) { - var err error - if loader.pps, err = loader.getPassProviders(); err != nil { - return multibls.PrivateKeys{}, err - } - if loader.kcp, err = loader.getKmsClientProvider(); err != nil { - return multibls.PrivateKeys{}, err - } - return loader.loadKeyFiles() -} + keyDir string + decrypters map[string]keyDecrypter -func (loader *blsDirLoader) getPassProviders() ([]passProvider, error) { - switch loader.passSrcType { - case PassSrcFile: - return []passProvider{loader.getFilePassProvider()}, nil - case PassSrcPrompt: - return []passProvider{loader.getPromptPassProvider()}, nil - case PassSrcAuto: - return []passProvider{ - loader.getFilePassProvider(), - loader.getPromptPassProvider(), - }, nil - default: - return nil, errors.New("unknown pass source type") - } + loadedSecrets []*bls_core.SecretKey } -func (loader *blsDirLoader) getFilePassProvider() passProvider { - if stringIsSet(loader.passFile) { - return newFilePassProvider(*loader.passFile) +func newBlsDirLoader(keyDir string, decrypters []keyDecrypter) (*blsDirLoader, error) { + dm := make(map[string]keyDecrypter) + for _, decrypter := range decrypters { + dm[decrypter.extension()] = decrypter } - return newDirPassProvider(loader.dirPath) -} - -func (loader *blsDirLoader) getPromptPassProvider() passProvider { - provider := newPromptPassProvider() - if loader.persistPassphrase { - provider.setPersist(loader.dirPath) + if err := checkIsDir(keyDir); err != nil { + return nil, fmt.Errorf("bls dir %v: %v", keyDir, err) } - return provider + return &blsDirLoader{ + keyDir: keyDir, + decrypters: dm, + }, nil } -func (loader *blsDirLoader) getKmsClientProvider() (kmsProvider, error) { - return newLazyKmsProvider(loader.kmsProviderConfig) -} - -func (loader *blsDirLoader) loadKeyFiles() (multibls.PrivateKeys, error) { - err := filepath.Walk(loader.dirPath, loader.processFileWalk) - if err != nil { - return multibls.PrivateKeys{}, err - } - return multibls.GetPrivateKeys(loader.secretKeys...), nil -} - -func (loader *blsDirLoader) processFileWalk(path string, info os.FileInfo, err error) error { - key, err := loader.loadKeyFromFile(path) - if err != nil { - if !errIsErrors(err, loader.skippingErrors()) { - // unexpected error, return the error and break the file walk loop +func (loader *blsDirLoader) loadKeys() (multibls.PrivateKeys, error) { + filepath.Walk(loader.keyDir, func(path string, info os.FileInfo, err error) error { + if err != nil { return err } - // errors to be skipped. Skipping these files - skipStr := fmt.Sprintf("Skipping [%s]: %v\n", path, err) - console.println(skipStr) - return nil - } - loader.secretKeys = append(loader.secretKeys, key) - return nil -} - -// errors to be neglected for directory bls loading -func (loader *blsDirLoader) skippingErrors() []error { - return []error{ - errUnknownExtension, - errNilPassProvider, - errNilKMSClientProvider, - } -} - -//func (loader *blsDirLoader) loadKeyFromFile(path string) (*bls_core.SecretKey, error) { -// var ( -// key *bls_core.SecretKey -// err error -// ) -// switch { -// case isBasicKeyFile(path): -// key, err = loadBasicKey(path, loader.pps) -// case isKMSKeyFile(path): -// key, err = loadKmsKeyFromFile(path, loader.kcp) -// default: -// err = errUnknownExtension -// } -// return key, err -//} - -// errIsErrors return whether the err is one of the errs -func errIsErrors(err error, errs []error) bool { - for _, targetErr := range errs { - if errors.Is(err, targetErr) { - return true + decrypter := loader.decrypters[filepath.Ext(path)] + secret, err := decrypter.decryptFile(path) + if err != nil { + return err } - } - return false + loader.loadedSecrets = append(loader.loadedSecrets, secret) + return nil + }) + return multibls.GetPrivateKeys(loader.loadedSecrets...), nil } diff --git a/cmd/harmony/blsloader/kms.go b/cmd/harmony/blsloader/kms.go new file mode 100644 index 000000000..c413b8dc7 --- /dev/null +++ b/cmd/harmony/blsloader/kms.go @@ -0,0 +1,286 @@ +package blsloader + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "sync" + "time" + + "github.com/harmony-one/harmony/internal/blsgen" + + "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" + bls_core "github.com/harmony-one/bls/ffi/go/bls" + "github.com/pkg/errors" +) + +// AwsConfigSrcType is the type of src to load aws config. Four options available: +// AwsCfgSrcNil - Disable kms decryption +// AwsCfgSrcFile - Provide the aws config through a file (json). +// AwsCfgSrcPrompt - Provide the aws config though prompt. +// AwsCfgSrcShared - Use the shard aws config (env -> default .aws directory) +type AwsCfgSrcType uint8 + +const ( + AwsCfgSrcNil AwsCfgSrcType = iota // nil place holder. + AwsCfgSrcFile // through a config file (json) + AwsCfgSrcPrompt // through console prompt. + AwsCfgSrcShared // through shared aws config +) + +func (srcType AwsCfgSrcType) isValid() bool { + switch srcType { + case AwsCfgSrcFile, AwsCfgSrcPrompt, AwsCfgSrcShared: + return true + default: + return false + } +} + +// kmsDecrypterConfig is the data structure of kmsClientProvider config +type kmsDecrypterConfig struct { + awsCfgSrcType AwsCfgSrcType + awsConfigFile *string +} + +// kmsDecrypter provide the kms client with singleton lazy initialization with config get +// from awsConfigProvider for aws credential and regions loading. +type kmsDecrypter struct { + config kmsDecrypterConfig + + provider awsConfigProvider + client *kms.KMS + err error + once sync.Once +} + +// newKmsDecrypter creates a kmsDecrypter with the given config +func newKmsDecrypter(config kmsDecrypterConfig) (*kmsDecrypter, error) { + kd := &kmsDecrypter{config: config} + if err := kd.validateConfig(); err != nil { + return nil, err + } + kd.makeACProvider() + return kd, nil +} + +// extension returns the kms key file extension +func (kd *kmsDecrypter) extension() string { + return kmsKeyExt +} + +// decryptFile decrypt a kms key file to a secret key +func (kd *kmsDecrypter) decryptFile(keyFile string) (*bls_core.SecretKey, error) { + kms, err := kd.getKMSClient() + if err != nil { + return nil, err + } + return blsgen.LoadAwsCMKEncryptedBLSKey(keyFile, kms) +} + +func (kd *kmsDecrypter) validateConfig() error { + config := kd.config + if !config.awsCfgSrcType.isValid() { + return errors.New("unknown AwsCfgSrcType") + } + if config.awsCfgSrcType == AwsCfgSrcFile { + if !stringIsSet(config.awsConfigFile) { + return errors.New("config field AwsConfig file must set for AwsCfgSrcFile") + } + if err := checkIsFile(*config.awsConfigFile); err != nil { + return fmt.Errorf("aws config file %v: %v", *config.awsConfigFile, err) + } + } + return nil +} + +func (kd *kmsDecrypter) makeACProvider() { + config := kd.config + switch config.awsCfgSrcType { + case AwsCfgSrcFile: + kd.provider = newFileACProvider(*config.awsConfigFile) + case AwsCfgSrcPrompt: + kd.provider = newPromptACProvider(defKmsPromptTimeout) + case AwsCfgSrcShared: + kd.provider = newSharedAwsConfigProvider() + } +} + +func (kd *kmsDecrypter) getKMSClient() (*kms.KMS, error) { + kd.once.Do(func() { + cfg, err := kd.provider.getAwsConfig() + if err != nil { + kd.err = err + return + } + kd.client, kd.err = kmsClientWithConfig(cfg) + }) + if kd.err != nil { + return nil, kd.err + } + return kd.client, nil +} + +// AwsConfig is the config data structure for credentials and region. Used for AWS KMS +// decryption. +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, + } +} + +// awsConfigProvider provides the aws config. Implemented by +// sharedACProvider - provide the nil to use shared AWS configuration +// fileACProvider - provide the aws config with a json file +// promptACProvider - provide the config field from prompt with time out +type awsConfigProvider interface { + getAwsConfig() (*AwsConfig, error) + String() string +} + +// sharedACProvider returns nil for getAwsConfig to use shared aws configurations +type sharedACProvider struct{} + +func newSharedAwsConfigProvider() *sharedACProvider { + return &sharedACProvider{} +} + +func (provider *sharedACProvider) getAwsConfig() (*AwsConfig, error) { + return nil, nil +} + +func (provider *sharedACProvider) String() string { + return "shared aws config" +} + +// fileACProvider get aws config through a customized json file +type fileACProvider struct { + file string +} + +func newFileACProvider(file string) *fileACProvider { + return &fileACProvider{file} +} + +func (provider *fileACProvider) 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 +} + +func (provider *fileACProvider) String() string { + return fmt.Sprintf("file %v", provider.file) +} + +// promptACProvider provide a user interactive console for AWS config. +// Four fields are asked: +// 1. AccessKey 2. SecretKey 3. Region +// Each field is asked with a timeout mechanism. +type promptACProvider struct { + timeout time.Duration +} + +func newPromptACProvider(timeout time.Duration) *promptACProvider { + return &promptACProvider{ + timeout: timeout, + } +} + +func (provider *promptACProvider) getAwsConfig() (*AwsConfig, error) { + console.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 *promptACProvider) 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 *promptACProvider) threadedPrompt(hint string) (string, error) { + console.print(hint) + return console.readPassword() +} + +func (provider *promptACProvider) String() string { + return "prompt" +} + +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/kmsProvider.go b/cmd/harmony/blsloader/kmsProvider.go deleted file mode 100644 index e4e700fe3..000000000 --- a/cmd/harmony/blsloader/kmsProvider.go +++ /dev/null @@ -1,249 +0,0 @@ -package blsloader - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "sync" - "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" -) - -// AwsConfig is the config data structure for credentials and region. Used for AWS KMS -// decryption. -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, - } -} - -// kmsProviderConfig is the data structure of kmsClientProvider config -type kmsProviderConfig struct { - awsCfgSrcType AwsCfgSrcType - awsConfigFile *string -} - -func (config kmsProviderConfig) validate() error { - if !config.awsCfgSrcType.isValid() { - return errors.New("unknown AwsCfgSrcType") - } - if config.awsCfgSrcType == AwsCfgSrcFile { - if !stringIsSet(config.awsConfigFile) { - return errors.New("config field AwsConfig file must set for AwsCfgSrcFile") - } - if !isFile(*config.awsConfigFile) { - return fmt.Errorf("aws config file not exist %v", *config.awsConfigFile) - } - } - return nil -} - -// kmsProvider provide the aws kms client -type kmsProvider interface { - getKMSClient() (*kms.KMS, error) -} - -// lazyKmsProvider provide the kms client with singleton lazy initialization with config get -// from awsConfigGetter for aws credential and regions loading. -type lazyKmsProvider struct { - acGetter awsConfigGetter - - client *kms.KMS - err error - once sync.Once -} - -// newLazyKmsProvider creates a kmsProvider with the given config -func newLazyKmsProvider(config kmsProviderConfig) (*lazyKmsProvider, error) { - var acg awsConfigGetter - switch config.awsCfgSrcType { - case AwsCfgSrcFile: - if stringIsSet(config.awsConfigFile) { - acg = newFileACGetter(*config.awsConfigFile) - } else { - acg = newSharedAwsConfigGetter() - } - case AwsCfgSrcPrompt: - acg = newPromptACGetter(defKmsPromptTimeout) - case AwsCfgSrcShared: - acg = newSharedAwsConfigGetter() - default: - return nil, errors.New("unknown aws config source type") - } - return &lazyKmsProvider{ - acGetter: acg, - }, nil -} - -func (provider *lazyKmsProvider) getKMSClient() (*kms.KMS, error) { - provider.once.Do(func() { - cfg, err := provider.acGetter.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 -} - -// awsConfigGetter provides the aws config. Implemented by -// sharedACGetter - provide the nil to use shared AWS configuration -// fileACGetter - provide the aws config with a json file -// promptACGetter - provide the config field from prompt with time out -type awsConfigGetter interface { - getAwsConfig() (*AwsConfig, error) - String() string -} - -// sharedACGetter returns nil for getAwsConfig to use shared aws configurations -type sharedACGetter struct{} - -func newSharedAwsConfigGetter() *sharedACGetter { - return &sharedACGetter{} -} - -func (getter *sharedACGetter) getAwsConfig() (*AwsConfig, error) { - return nil, nil -} - -func (getter *sharedACGetter) String() string { - return "shared aws config" -} - -// fileACGetter get aws config through a customized json file -type fileACGetter struct { - file string -} - -func newFileACGetter(file string) *fileACGetter { - return &fileACGetter{file} -} - -func (getter *fileACGetter) getAwsConfig() (*AwsConfig, error) { - b, err := ioutil.ReadFile(getter.file) - if err != nil { - return nil, err - } - var cfg AwsConfig - if err := json.Unmarshal(b, &cfg); err != nil { - return nil, err - } - return &cfg, nil -} - -func (getter *fileACGetter) String() string { - return fmt.Sprintf("file %v", getter.file) -} - -// promptACGetter provide a user interactive console for AWS config. -// Four fields are asked: -// 1. AccessKey 2. SecretKey 3. Region -// Each field is asked with a timeout mechanism. -type promptACGetter struct { - timeout time.Duration -} - -func newPromptACGetter(timeout time.Duration) *promptACGetter { - return &promptACGetter{ - timeout: timeout, - } -} - -func (getter *promptACGetter) getAwsConfig() (*AwsConfig, error) { - console.println("Please provide AWS configurations for KMS encoded BLS keys:") - accessKey, err := getter.prompt(" AccessKey:") - if err != nil { - return nil, fmt.Errorf("cannot get aws access key: %v", err) - } - secretKey, err := getter.prompt(" SecretKey:") - if err != nil { - return nil, fmt.Errorf("cannot get aws secret key: %v", err) - } - region, err := getter.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 (getter *promptACGetter) prompt(hint string) (string, error) { - var ( - res string - err error - - finished = make(chan struct{}) - timedOut = time.After(getter.timeout) - ) - - go func() { - res, err = getter.threadedPrompt(hint) - close(finished) - }() - - for { - select { - case <-finished: - return res, err - case <-timedOut: - return "", errors.New("timed out") - } - } -} - -func (getter *promptACGetter) threadedPrompt(hint string) (string, error) { - console.print(hint) - return console.readPassword() -} - -func (getter *promptACGetter) String() string { - return "prompt" -} - -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/loader.go b/cmd/harmony/blsloader/loader.go index daa9d1c15..7fd5ea5b0 100644 --- a/cmd/harmony/blsloader/loader.go +++ b/cmd/harmony/blsloader/loader.go @@ -3,31 +3,72 @@ package blsloader import ( "errors" "fmt" - "path/filepath" + bls_core "github.com/harmony-one/bls/ffi/go/bls" "github.com/harmony-one/harmony/multibls" ) func LoadKeys(cfg Config) (multibls.PrivateKeys, error) { - cfg.applyDefault() - if err := cfg.validate(); err != nil { - return multibls.PrivateKeys{}, err + decrypters, err := getKeyDecrypters(cfg) + if err != nil { + return nil, err } - helper, err := getHelper(cfg) + helper, err := getHelper(cfg, decrypters) if err != nil { return nil, err } return helper.loadKeys() } +// keyDecrypter is the interface to decrypt the bls key file. Currently, two +// implementations are supported: +// passDecrypter - decrypt with passphrase +// kmsDecrypter - decrypt with aws kms service +type keyDecrypter interface { + extension() string + decryptFile(keyFile string) (*bls_core.SecretKey, error) +} + +func getKeyDecrypters(cfg Config) ([]keyDecrypter, error) { + var decrypters []keyDecrypter + if cfg.PassSrcType != PassSrcNil { + pd, err := newPassDecrypter(cfg.getPassProviderConfig()) + if err != nil { + return nil, err + } + decrypters = append(decrypters, pd) + } + if cfg.AwsCfgSrcType != AwsCfgSrcNil { + kd, err := newKmsDecrypter(cfg.getKmsProviderConfig()) + if err != nil { + return nil, err + } + decrypters = append(decrypters, kd) + } + if len(decrypters) == 0 { + return nil, fmt.Errorf("must provide at least one bls key decryption") + } + return decrypters, nil +} + +func getHelper(cfg Config, decrypters []keyDecrypter) (loadHelper, error) { + switch { + case len(cfg.multiBlsKeys) != 0: + return newMultiKeyLoader(cfg.multiBlsKeys, decrypters) + case stringIsSet(cfg.BlsDir): + return newBlsDirLoader(*cfg.BlsDir, decrypters) + default: + return nil, errors.New("either multiBlsKeys or BlsDir must be set") + } +} + // Loader is the structure to load bls keys. type Config struct { - // source for bls key loading. At least one of the BlsKeyFile and BlsDir + // source for bls key loading. At least one of the multiBlsKeys and BlsDir // need to be provided. // - // BlsKeyFile defines a single key file to load from. Based on the file - // extension, decryption with either passphrase or aws kms will be used. - BlsKeyFile *string + // multiBlsKeys defines a slice of key files to load from. + multiBlsKeys []string // BlsDir defines a file directory to load keys from. BlsDir *string @@ -47,7 +88,7 @@ type Config struct { // under the same directory as the key file. PersistPassphrase bool - // Aws configuration related settings, including AWS credentials and region info. + // KMS related settings, including AWS credentials and region info. // Used for KMS encrypted passphrase files. // // AwsCfgSrcType defines the source to get aws config. Three types available: @@ -60,38 +101,6 @@ type Config struct { AwsConfigFile *string } -func (cfg *Config) applyDefault() { - if cfg.PassSrcType == PassSrcNil { - cfg.PassSrcType = PassSrcAuto - } - if cfg.AwsCfgSrcType == AwsCfgSrcNil { - cfg.AwsCfgSrcType = AwsCfgSrcShared - } -} - -func (cfg *Config) validate() error { - if stringIsSet(cfg.BlsKeyFile) { - if !isFile(*cfg.BlsKeyFile) { - return fmt.Errorf("key file not exist %v", *cfg.BlsKeyFile) - } - switch ext := filepath.Ext(*cfg.BlsKeyFile); ext { - case basicKeyExt, kmsKeyExt: - default: - return fmt.Errorf("unknown key file extension %v", ext) - } - } else if stringIsSet(cfg.BlsDir) { - if !isDir(*cfg.BlsDir) { - return fmt.Errorf("dir not exist %v", *cfg.BlsDir) - } - } else { - return errors.New("either BlsKeyFile or BlsDir must be set") - } - if err := cfg.getPassProviderConfig().validate(); err != nil { - return err - } - return cfg.getKmsProviderConfig().validate() -} - func (cfg *Config) getPassProviderConfig() passDecrypterConfig { return passDecrypterConfig{ passSrcType: cfg.PassSrcType, @@ -100,40 +109,9 @@ func (cfg *Config) getPassProviderConfig() passDecrypterConfig { } } -func (cfg *Config) getKmsProviderConfig() kmsProviderConfig { - return kmsProviderConfig{ +func (cfg *Config) getKmsProviderConfig() kmsDecrypterConfig { + return kmsDecrypterConfig{ awsCfgSrcType: cfg.AwsCfgSrcType, awsConfigFile: cfg.AwsConfigFile, } } - -func getHelper(cfg Config) (loadHelper, error) { - fmt.Println("getting helper") - switch { - case stringIsSet(cfg.BlsKeyFile): - switch filepath.Ext(*cfg.BlsKeyFile) { - case basicKeyExt: - fmt.Println("basic") - return &basicSingleBlsLoader{ - blsKeyFile: *cfg.BlsKeyFile, - passDecrypterConfig: cfg.getPassProviderConfig(), - }, nil - case kmsKeyExt: - fmt.Println("kms") - return &kmsSingleBlsLoader{ - blsKeyFile: *cfg.BlsKeyFile, - kmsProviderConfig: cfg.getKmsProviderConfig(), - }, nil - default: - return nil, errors.New("unknown extension") - } - case stringIsSet(cfg.BlsDir): - return &blsDirLoader{ - dirPath: *cfg.BlsDir, - passDecrypterConfig: cfg.getPassProviderConfig(), - kmsProviderConfig: cfg.getKmsProviderConfig(), - }, nil - default: - return nil, errors.New("either BlsKeyFile or BlsDir must be set") - } -} diff --git a/cmd/harmony/blsloader/params.go b/cmd/harmony/blsloader/params.go index 653330bfe..783dff08f 100644 --- a/cmd/harmony/blsloader/params.go +++ b/cmd/harmony/blsloader/params.go @@ -19,25 +19,3 @@ const ( defWritePassDirMode = 0600 defWritePassFileMode = 0600 ) - -// AwsConfigSrcType is the type of src to load aws config. Two options available -// AwsCfgSrcFile - Provide the aws config through a file (json). -// AwsCfgSrcPrompt - Provide the aws config though prompt. -// AwsCfgSrcShared - Use the shard aws config (env -> default .aws directory) -type AwsCfgSrcType uint8 - -const ( - AwsCfgSrcNil AwsCfgSrcType = iota // nil place holder. - AwsCfgSrcFile // through a config file (json) - AwsCfgSrcPrompt // through console prompt. - AwsCfgSrcShared // through shared aws config -) - -func (srcType AwsCfgSrcType) isValid() bool { - switch srcType { - case AwsCfgSrcFile, AwsCfgSrcPrompt, AwsCfgSrcShared: - return true - default: - return false - } -} diff --git a/cmd/harmony/blsloader/passProvider.go b/cmd/harmony/blsloader/passphrase.go similarity index 94% rename from cmd/harmony/blsloader/passProvider.go rename to cmd/harmony/blsloader/passphrase.go index 05669162d..2043fe064 100644 --- a/cmd/harmony/blsloader/passProvider.go +++ b/cmd/harmony/blsloader/passphrase.go @@ -12,7 +12,8 @@ import ( ) // PassSrcType is the type of passphrase provider source. -// Three options available: +// Four options available: +// PassSrcNil - Do not use passphrase decryption // PassSrcFile - Read the passphrase from files // PassSrcPrompt - Read the passphrase from prompt // PassSrcAuto - First try to unlock with passphrase from file, then read passphrase from prompt @@ -51,7 +52,7 @@ type passDecrypter struct { func newPassDecrypter(cfg passDecrypterConfig) (*passDecrypter, error) { pd := &passDecrypter{config: cfg} - if err := pd.validate(); err != nil { + if err := pd.validateConfig(); err != nil { return nil, err } pd.makePassProviders() @@ -62,13 +63,13 @@ func (pd *passDecrypter) extension() string { return basicKeyExt } -func (pd *passDecrypter) validate() error { +func (pd *passDecrypter) validateConfig() error { config := pd.config if !config.passSrcType.isValid() { return errors.New("unknown PassSrcType") } if stringIsSet(config.passFile) { - if err := isPassFile(*config.passFile); err != nil { + if err := checkIsPassFile(*config.passFile); err != nil { return fmt.Errorf("%v not a passphrase file: %v", *config.passFile, err) } } @@ -102,7 +103,7 @@ func (pd *passDecrypter) getFilePassProvider() passProvider { } } -func (pd *passDecrypter) decrypt(keyFile string) (*bls_core.SecretKey, error) { +func (pd *passDecrypter) decryptFile(keyFile string) (*bls_core.SecretKey, error) { for _, pp := range pd.pps { secretKey, err := loadBasicKeyWithProvider(keyFile, pp) if err != nil { diff --git a/cmd/harmony/blsloader/utils.go b/cmd/harmony/blsloader/utils.go index 603d73639..a3c2eb456 100644 --- a/cmd/harmony/blsloader/utils.go +++ b/cmd/harmony/blsloader/utils.go @@ -1,71 +1,28 @@ package blsloader import ( - "fmt" "os" "path/filepath" "strings" - "github.com/pkg/errors" - bls_core "github.com/harmony-one/bls/ffi/go/bls" "github.com/harmony-one/harmony/internal/blsgen" + "github.com/pkg/errors" ) -var ( - errUnknownExtension = errors.New("unknown extension") - errUnableGetPubkey = errors.New("unable to get public key") - errNilPassProvider = errors.New("no source for password") - errNilKMSClientProvider = errors.New("no source for KMS provider") -) - -// loadBasicKey loads a single bls key through a key file and passphrase combination. -// The passphrase is provided by a slice of passProviders. -func loadBasicKey(blsKeyFile string, pps []passProvider) (*bls_core.SecretKey, error) { - if len(pps) == 0 { - return nil, errNilPassProvider - } - for _, pp := range pps { - secretKey, err := loadBasicKeyWithProvider(blsKeyFile, pp) - if err != nil { - console.println(err) - continue - } - return secretKey, nil - } - return nil, fmt.Errorf("failed to load bls key %v", blsKeyFile) -} - func loadBasicKeyWithProvider(blsKeyFile string, pp passProvider) (*bls_core.SecretKey, error) { pass, err := pp.getPassphrase(blsKeyFile) if err != nil { - return nil, errors.Wrapf(err, "unable to get passphrase from %s", pp.toStr()) + return nil, err } - fmt.Printf("password: %s\n", pass) secretKey, err := blsgen.LoadBLSKeyWithPassPhrase(blsKeyFile, pass) if err != nil { - return nil, errors.Wrapf(err, "unable to decrypt bls key with %s\n", pp.toStr()) + return nil, err } return secretKey, nil } -// loadKmsKeyFromFile loads a single KMS BLS key from file -func loadKmsKeyFromFile(blsKeyFile string, kcp kmsProvider) (*bls_core.SecretKey, error) { - if kcp == nil { - return nil, errNilKMSClientProvider - } - client, err := kcp.getKMSClient() - if err != nil { - return nil, errors.Wrap(err, "failed to get KMS client") - } - secretKey, err := blsgen.LoadAwsCMKEncryptedBLSKey(blsKeyFile, client) - if err != nil { - return nil, errors.Wrap(err, "failed to load KMS BLS key") - } - return secretKey, nil -} - -func isFile(path string) error { +func checkIsFile(path string) error { info, err := os.Stat(path) if err != nil { return err @@ -76,7 +33,7 @@ func isFile(path string) error { return nil } -func isDir(path string) error { +func checkIsDir(path string) error { info, err := os.Stat(path) if err != nil { return err @@ -87,19 +44,8 @@ func isDir(path string) error { return nil } -func isBasicKeyFile(path string) error { - err := isFile(path) - if err != nil { - return err - } - if filepath.Ext(path) != basicKeyExt { - return errors.New("should have extension .key") - } - return nil -} - -func isPassFile(path string) error { - err := isFile(path) +func checkIsPassFile(path string) error { + err := checkIsFile(path) if err != nil { return err } @@ -109,17 +55,6 @@ func isPassFile(path string) error { return nil } -func isKMSKeyFile(path string) error { - err := isFile(path) - if err != nil { - return err - } - if filepath.Ext(path) != kmsKeyExt { - return errors.New("should have extension .bls") - } - return nil -} - func keyFileToPassFileFull(keyFile string) string { return strings.Trim(keyFile, basicKeyExt) + passExt } @@ -128,9 +63,7 @@ func promptGetPassword(prompt string) (string, error) { if !strings.HasSuffix(prompt, ":") { prompt += ":" } - fmt.Println("before print prompt", prompt) console.print(prompt) - fmt.Println("after print prompt", prompt) return console.readPassword() }