Skip to content

Commit

Permalink
DX-7509: redact livemode values in existing configs (#918)
Browse files Browse the repository at this point in the history
* DX-7509: redact livemode values in existing configs

* fix import spacings
  • Loading branch information
etsai-stripe committed Jul 21, 2022
1 parent 88f09d5 commit 1f85fef
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 51 deletions.
18 changes: 18 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,24 @@ func (c *Config) InitConfig() {
KeyRing, _ = keyring.Open(keyring.Config{
ServiceName: "Stripe CLI Key Storage",
})

// redact livemode values for existing configs
if err := viper.ReadInConfig(); err == nil {
// if the config file has expires at date, then it is using the new livemode key storage
if viper.IsSet(c.Profile.GetConfigField(LiveModeAPIKeyName)) {
key := viper.GetString(c.Profile.GetConfigField(LiveModeAPIKeyName))
if !IsRedactedAPIKey(key) {
c.Profile.WriteConfigField(LiveModeAPIKeyName, RedactAPIKey(key))
}
}

if viper.IsSet(c.Profile.GetConfigField(LiveModePubKeyName)) {
key := viper.GetString(c.Profile.GetConfigField(LiveModePubKeyName))
if !IsRedactedAPIKey(key) {
c.Profile.WriteConfigField(LiveModePubKeyName, RedactAPIKey(key))
}
}
}
}

// EditConfig opens the configuration file in the default editor.
Expand Down
109 changes: 62 additions & 47 deletions pkg/config/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ const (
DisplayNameName = "display_name"
IsTermsAcceptanceValidName = "is_terms_acceptance_valid"
TestModeAPIKeyName = "test_mode_api_key"
TestModePublishableKeyName = "test_mode_publishable_key"
TestModePubKeyName = "test_mode_pub_key"
TestModeKeyExpiresAtName = "test_mode_key_expires_at"
LiveModeAPIKeyName = "live_mode_api_key"
LiveModePublishableKeyName = "live_mode_publishable_key"
LiveModePubKeyName = "live_mode_pub_key"
LiveModeKeyExpiresAtName = "live_mode_key_expires_at"
)

Expand Down Expand Up @@ -122,32 +122,34 @@ func (p *Profile) GetAPIKey(livemode bool) (string, error) {
return p.APIKey, nil
}

// If the user doesn't have an api_key field set, they might be using an
// old configuration so try to read from secret_key
var key string
var err error

// Try to fetch the API key from the configuration file
if !livemode {
if !viper.IsSet(p.GetConfigField("api_key")) {
p.RegisterAlias("api_key", "secret_key")
} else {
// If the user doesn't have an api_key field set, they might be using an
// old configuration so try to read from secret_key
if viper.IsSet(p.GetConfigField("secret_key")) {
p.RegisterAlias(TestModeAPIKeyName, "secret_key")
} else if viper.IsSet(p.GetConfigField("api_key")) {
p.RegisterAlias(TestModeAPIKeyName, "api_key")
}
}

// Try to fetch the API key from the configuration file
if err := viper.ReadInConfig(); err == nil {
var key string
fieldID := livemodeKeyField(livemode)

if !livemode {
key = viper.GetString(p.GetConfigField(fieldID))
} else {
key = p.RetrieveLivemodeValue(fieldID)
if err := viper.ReadInConfig(); err == nil {
key = viper.GetString(p.GetConfigField(TestModeAPIKeyName))
}

err := validators.APIKey(key)
} else {
key, err = p.RetrieveLivemodeValue(LiveModeAPIKeyName)
if err != nil {
return "", err
}
}

if key != "" {
err = validators.APIKey(key)
if err != nil {
return "", err
}
return key, nil
}

Expand All @@ -157,43 +159,59 @@ func (p *Profile) GetAPIKey(livemode bool) (string, error) {
// GetExpiresAt returns the API key expirary date
func (p *Profile) GetExpiresAt(livemode bool) (time.Time, error) {
var timeString string
fieldID := TestModeKeyExpiresAtName
var err error

if livemode {
fieldID = LiveModeKeyExpiresAtName
timeString = p.RetrieveLivemodeValue(p.GetConfigField(fieldID))
timeString, err = p.RetrieveLivemodeValue(LiveModeKeyExpiresAtName)
if err != nil {
return time.Time{}, err
}
} else {
timeString = viper.GetString(p.GetConfigField(fieldID))
timeString = viper.GetString(p.GetConfigField(TestModeKeyExpiresAtName))
}

expiresAt, err := time.Parse(DateStringFormat, timeString)

if err != nil {
return time.Time{}, err
if timeString != "" {
expiresAt, err := time.Parse(DateStringFormat, timeString)
if err != nil {
return time.Time{}, err
}
return expiresAt, nil
}

return expiresAt, nil
return time.Time{}, validators.ErrAPIKeyNotConfigured
}

// GetPublishableKey returns the publishable key for the user
func (p *Profile) GetPublishableKey(livemode bool) string {
key := ""
fieldID := TestModePublishableKeyName
func (p *Profile) GetPublishableKey(livemode bool) (string, error) {
var key string
var err error

if livemode {
fieldID = LiveModePublishableKeyName
key = p.RetrieveLivemodeValue(p.GetConfigField(fieldID))
key, err = p.RetrieveLivemodeValue(LiveModePubKeyName)
if err != nil {
return "", err
}
} else {
// test mode
if err := viper.ReadInConfig(); err == nil {
if viper.IsSet(p.GetConfigField("publishable_key")) {
p.RegisterAlias(fieldID, "publishable_key")
p.RegisterAlias(TestModePubKeyName, "publishable_key")
}
// there is a bug with viper.GetStringMapString when the key name is too long, which makes
// `config --list --project-name <project_name>` unable to read the project specific config
if viper.IsSet(p.GetConfigField("test_mode_publishable_key")) {
p.RegisterAlias(TestModePubKeyName, "test_mode_publishable_key")
}

key = viper.GetString(p.GetConfigField(fieldID))
key = viper.GetString(p.GetConfigField(TestModePubKeyName))
}
}

return key
if key != "" {
return key, nil
}

return "", validators.ErrAPIKeyNotConfigured
}

// GetDisplayName returns the account display name of the user
Expand Down Expand Up @@ -238,6 +256,11 @@ func (p *Profile) DeleteConfigField(field string) error {
return err
}

// delete livemode redacted values from config and full values from keyring
if field == LiveModeAPIKeyName || field == LiveModePubKeyName || field == LiveModeKeyExpiresAtName {
p.DeleteLivemodeValue(field)
}

return p.writeProfile(v)
}

Expand Down Expand Up @@ -267,10 +290,10 @@ func (p *Profile) writeProfile(runtimeViper *viper.Viper) error {

if p.LiveModePublishableKey != "" {
// store redacted key in config
runtimeViper.Set(p.GetConfigField(LiveModePublishableKeyName), RedactAPIKey(strings.TrimSpace(p.LiveModePublishableKey)))
runtimeViper.Set(p.GetConfigField(LiveModePubKeyName), RedactAPIKey(strings.TrimSpace(p.LiveModePublishableKey)))

// store actual key in secure keyring
p.storeLivemodeValue(LiveModePublishableKeyName, strings.TrimSpace(p.LiveModePublishableKey), "Live mode publishable key")
p.storeLivemodeValue(LiveModePubKeyName, strings.TrimSpace(p.LiveModePublishableKey), "Live mode publishable key")
}

if p.TestModeAPIKey != "" {
Expand All @@ -279,7 +302,7 @@ func (p *Profile) writeProfile(runtimeViper *viper.Viper) error {
}

if p.TestModePublishableKey != "" {
runtimeViper.Set(p.GetConfigField(TestModePublishableKeyName), strings.TrimSpace(p.TestModePublishableKey))
runtimeViper.Set(p.GetConfigField(TestModePubKeyName), strings.TrimSpace(p.TestModePublishableKey))
}

if p.DisplayName != "" {
Expand Down Expand Up @@ -328,14 +351,6 @@ func (p *Profile) safeRemove(v *viper.Viper, key string) *viper.Viper {
return v
}

func livemodeKeyField(livemode bool) string {
if livemode {
return LiveModeAPIKeyName
}

return TestModeAPIKeyName
}

func getKeyExpiresAt() string {
return time.Now().AddDate(0, 0, KeyValidInDays).UTC().Format(DateStringFormat)
}
53 changes: 50 additions & 3 deletions pkg/config/profile_livemode.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"strings"

"github.com/99designs/keyring"

"github.com/stripe/stripe-cli/pkg/validators"
)

// DateStringFormat ...
Expand All @@ -27,10 +29,37 @@ func (p *Profile) storeLivemodeValue(field, value, description string) {
}

// RetrieveLivemodeValue ...
func (p *Profile) RetrieveLivemodeValue(key string) string {
func (p *Profile) RetrieveLivemodeValue(key string) (string, error) {
fieldID := p.GetConfigField(key)
existingKeys, err := KeyRing.Keys()
if err != nil {
return "", err
}

for _, item := range existingKeys {
if item == fieldID {
value, _ := KeyRing.Get(fieldID)
return string(value.Data), nil
}
}

return "", validators.ErrAPIKeyNotConfigured
}

// DeleteLivemodeValue ...
func (p *Profile) DeleteLivemodeValue(key string) error {
fieldID := p.GetConfigField(key)
value, _ := KeyRing.Get(fieldID)
return string(value.Data)
existingKeys, err := KeyRing.Keys()
if err != nil {
return err
}
for _, item := range existingKeys {
if item == fieldID {
KeyRing.Remove(fieldID)
return nil
}
}
return nil
}

// RedactAPIKey returns a redacted version of API keys. The first 8 and last 4
Expand All @@ -46,3 +75,21 @@ func RedactAPIKey(apiKey string) string {

return b.String()
}

// IsRedactedAPIKey ...
func IsRedactedAPIKey(apiKey string) bool {
keyParts := strings.Split(apiKey, "_")
if len(keyParts) < 3 {
return false
}

if keyParts[0] != "sk" && keyParts[0] != "rk" {
return false
}

if RedactAPIKey(apiKey) != apiKey {
return false
}

return true
}
2 changes: 1 addition & 1 deletion pkg/samples/samples.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ func (s *Samples) ConfigureDotEnv(ctx context.Context, sampleLocation string) er
return err
}

publishableKey := s.Config.Profile.GetPublishableKey(false)
publishableKey, _ := s.Config.Profile.GetPublishableKey(false)
if publishableKey == "" {
return fmt.Errorf("we could not set the publishable key in the .env file; please set this manually or login again to set it automatically next time")
}
Expand Down

0 comments on commit 1f85fef

Please sign in to comment.