Skip to content

Commit

Permalink
Re-enable livemode key management (#982)
Browse files Browse the repository at this point in the history
* wip

* wip

* refactor

* remove comments

* remove extra imports

* remove unused imports

* fix typo

* bug fix

* remove livemode api key at logout
  • Loading branch information
etsai-stripe authored Oct 13, 2022
1 parent 65571fc commit e0b09ae
Show file tree
Hide file tree
Showing 8 changed files with 250 additions and 126 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ require (
)

require (
github.com/alessio/shellescape v1.4.1
github.com/hashicorp/go-hclog v1.2.2
github.com/hashicorp/go-plugin v1.4.4
github.com/joho/godotenv v1.4.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VM
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0=
github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
Expand Down
8 changes: 6 additions & 2 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,11 +163,11 @@ func (c *Config) InitConfig() {

// initialize key ring
KeyRing, _ = keyring.Open(keyring.Config{
ServiceName: "Stripe CLI Key Storage",
ServiceName: KeyManagementService,
})

// redact livemode values for existing configs
// c.Profile.redactAllLivemodeValues()
c.Profile.redactAllLivemodeValues()
}

// EditConfig opens the configuration file in the default editor.
Expand Down Expand Up @@ -244,6 +244,8 @@ func (c *Config) RemoveProfile(profileName string) error {
if err != nil {
return err
}

deleteLivemodeKey(LiveModeAPIKeyName, field)
}
}

Expand All @@ -261,6 +263,8 @@ func (c *Config) RemoveAllProfiles() error {
if err != nil {
return err
}

deleteLivemodeKey(LiveModeAPIKeyName, field)
}
}

Expand Down
19 changes: 19 additions & 0 deletions pkg/config/config_livemode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//go:build !arm64
// +build !arm64

package config

func deleteLivemodeKey(key string, profile string) error {
fieldID := profile + "." + key
existingKeys, err := KeyRing.Keys()
if err != nil {
return err
}
for _, item := range existingKeys {
if item == fieldID {
KeyRing.Remove(fieldID)
return nil
}
}
return nil
}
19 changes: 19 additions & 0 deletions pkg/config/config_livemode_arm64.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//go:build arm64
// +build arm64

package config

import exec "golang.org/x/sys/execabs"

func deleteLivemodeKey(key string, profile string) error {
fieldID := profile + "." + key
_, err := exec.Command(
execPathKeychain,
"delete-generic-password",
"-s", fieldID,
"-a", KeyManagementService).CombinedOutput()
if err != nil {
return err
}
return nil
}
107 changes: 78 additions & 29 deletions pkg/config/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import (
"strings"
"time"

"github.com/99designs/keyring"
"github.com/spf13/viper"

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

Expand Down Expand Up @@ -40,6 +42,20 @@ const (
LiveModeKeyExpiresAtName = "live_mode_key_expires_at"
)

const (
// DateStringFormat is the format for expiredAt date
DateStringFormat = "2006-01-02"

// KeyValidInDays is the number of days the API key is valid for
KeyValidInDays = 90

// KeyManagementService is the key management service name
KeyManagementService = "Stripe CLI"
)

// KeyRing ...
var KeyRing keyring.Keyring

// CreateProfile creates a profile when logging in
func (p *Profile) CreateProfile() error {
writeErr := p.writeProfile(viper.GetViper())
Expand Down Expand Up @@ -139,18 +155,10 @@ func (p *Profile) GetAPIKey(livemode bool) (string, error) {
key = viper.GetString(p.GetConfigField(TestModeAPIKeyName))
}
} else {
// p.redactAllLivemodeValues()
// key, err = p.retrieveLivemodeValue(LiveModeAPIKeyName)
// if err != nil {
// return "", err
// }

if err := viper.ReadInConfig(); err == nil {
key = viper.GetString(p.GetConfigField(LiveModeAPIKeyName))
}

if isRedactedAPIKey(key) {
return "", validators.ErrAPIKeyNotConfigured
p.redactAllLivemodeValues()
key, err = p.retrieveLivemodeValue(LiveModeAPIKeyName)
if err != nil {
return "", err
}
}

Expand All @@ -168,13 +176,8 @@ 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
// var err error

if livemode {
// timeString, err = p.retrieveLivemodeValue(LiveModeKeyExpiresAtName)
// if err != nil {
// return time.Time{}, err
// }
timeString = viper.GetString(p.GetConfigField(LiveModeKeyExpiresAtName))
} else {
timeString = viper.GetString(p.GetConfigField(TestModeKeyExpiresAtName))
Expand Down Expand Up @@ -267,9 +270,9 @@ func (p *Profile) DeleteConfigField(field string) error {
}

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

return p.writeProfile(v)
}
Expand All @@ -287,19 +290,14 @@ func (p *Profile) writeProfile(runtimeViper *viper.Viper) error {
}

if p.LiveModeAPIKey != "" {
// comment out livemode storage until bugs are ironed out
// expiresAt := getKeyExpiresAt()
expiresAt := getKeyExpiresAt()
runtimeViper.Set(p.GetConfigField(LiveModeKeyExpiresAtName), expiresAt)

// // store redacted key in config
// runtimeViper.Set(p.GetConfigField(LiveModeAPIKeyName), RedactAPIKey(strings.TrimSpace(p.LiveModeAPIKey)))
// runtimeViper.Set(p.GetConfigField(LiveModeKeyExpiresAtName), expiresAt)
runtimeViper.Set(p.GetConfigField(LiveModeAPIKeyName), RedactAPIKey(strings.TrimSpace(p.LiveModeAPIKey)))

// // store actual key in secure keyring
// p.saveLivemodeValue(LiveModeAPIKeyName, strings.TrimSpace(p.LiveModeAPIKey), "Live mode API key")
// p.saveLivemodeValue(LiveModeKeyExpiresAtName, expiresAt, "Live mode API key expirary")

runtimeViper.Set(p.GetConfigField(LiveModeAPIKeyName), strings.TrimSpace(p.LiveModeAPIKey))
runtimeViper.Set(p.GetConfigField(LiveModeKeyExpiresAtName), getKeyExpiresAt())
p.saveLivemodeValue(LiveModeAPIKeyName, strings.TrimSpace(p.LiveModeAPIKey), "Live mode API key")
}

if p.LiveModePublishableKey != "" {
Expand Down Expand Up @@ -361,6 +359,57 @@ func (p *Profile) safeRemove(v *viper.Viper, key string) *viper.Viper {
return v
}

// redactAllLivemodeValues redacts all livemode values in the local config file
func (p *Profile) redactAllLivemodeValues() {
color := ansi.Color(os.Stdout)

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(p.GetConfigField(LiveModeAPIKeyName)) {
key := viper.GetString(p.GetConfigField(LiveModeAPIKeyName))
if !isRedactedAPIKey(key) {
fmt.Println(color.Yellow(`
(!) Livemode value found for the field '` + LiveModeAPIKeyName + `' in your config file.
Livemode values from the config file will be redacted and will not be used.`))

p.WriteConfigField(LiveModeAPIKeyName, RedactAPIKey(key))
}
}
}
}

// RedactAPIKey returns a redacted version of API keys. The first 8 and last 4
// characters are not redacted, everything else is replaced by "*" characters.
//
// It panics if the provided string has less than 12 characters.
func RedactAPIKey(apiKey string) string {
var b strings.Builder

b.WriteString(apiKey[0:8]) // #nosec G104 (gosec bug: https://github.com/securego/gosec/issues/267)
b.WriteString(strings.Repeat("*", len(apiKey)-12)) // #nosec G104 (gosec bug: https://github.com/securego/gosec/issues/267)
b.WriteString(apiKey[len(apiKey)-4:]) // #nosec G104 (gosec bug: https://github.com/securego/gosec/issues/267)

return b.String()
}

// isRedactedAPIKey checks if the input string is a refacted api key
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
}

func getKeyExpiresAt() string {
return time.Now().AddDate(0, 0, KeyValidInDays).UTC().Format(DateStringFormat)
}
Loading

0 comments on commit e0b09ae

Please sign in to comment.