Skip to content

Commit

Permalink
Save auth tokens to the OS keyring
Browse files Browse the repository at this point in the history
This prevents tokens from being written to the filesystem. Supported on macOS, Windows, and Linux.
  • Loading branch information
Piccirello committed Oct 28, 2020
1 parent aeb3de3 commit 95b8b50
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 2 deletions.
2 changes: 2 additions & 0 deletions go.mod
Expand Up @@ -6,13 +6,15 @@ require (
github.com/AlecAivazis/survey/v2 v2.0.8
github.com/atotto/clipboard v0.1.2
github.com/go-openapi/strfmt v0.19.3 // indirect
github.com/google/uuid v1.1.1
github.com/jedib0t/go-pretty v4.3.0+incompatible
github.com/mattn/go-colorable v0.1.4 // indirect
github.com/mattn/go-isatty v0.0.10 // indirect
github.com/mattn/go-runewidth v0.0.5 // indirect
github.com/skratchdot/open-golang v0.0.0-20190402232053-79abb63cd66e
github.com/spf13/cobra v0.0.5
github.com/spf13/pflag v1.0.5 // indirect
github.com/zalando/go-keyring v0.1.0
go.mongodb.org/mongo-driver v1.1.2 // indirect
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5
golang.org/x/sys v0.0.0-20191029155521-f43be2a4598c // indirect
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Expand Up @@ -14,6 +14,7 @@ github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/danieljoos/wincred v1.0.2/go.mod h1:SnuYRW9lp1oJrZX/dXJqr0cPK5gYXqx3EJbmjhLdK9U=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand All @@ -24,6 +25,8 @@ github.com/go-openapi/strfmt v0.19.3 h1:eRfyY5SkaNJCAwmmMcADjY31ow9+N7MCLW7oRkbs
github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
Expand Down Expand Up @@ -78,10 +81,13 @@ github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/zalando/go-keyring v0.1.0 h1:ffq972Aoa4iHNzBlUHgK5Y+k8+r/8GvcGd80/OFZb/k=
github.com/zalando/go-keyring v0.1.0/go.mod h1:RaxNwUITJaHVdQ0VC7pELPZ3tOWn13nr0gZMZEhpVU0=
go.mongodb.org/mongo-driver v1.0.3 h1:GKoji1ld3tw2aC+GX1wbr/J2fX13yNacEYoJ8Nhr0yU=
go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
go.mongodb.org/mongo-driver v1.1.2 h1:jxcFYjlkl8xaERsgLo+RNquI0epW6zuy/ZRQs6jnrFA=
Expand Down
70 changes: 68 additions & 2 deletions pkg/configuration/config.go
Expand Up @@ -25,6 +25,7 @@ import (
"strconv"
"strings"

"github.com/DopplerHQ/cli/pkg/controllers"
"github.com/DopplerHQ/cli/pkg/models"
"github.com/DopplerHQ/cli/pkg/utils"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -143,6 +144,16 @@ func Get(scope string) models.ScopedOptions {
}
}

if controllers.IsKeyringSecret(scopedConfig.Token.Value) {
utils.LogDebug(fmt.Sprintf("Retrieving %s from system keyring", models.ConfigToken.String()))
token, err := controllers.GetKeyring(scopedConfig.Token.Value)
if !err.IsNil() {
utils.HandleError(err.Unwrap(), err.Message)
}

scopedConfig.Token.Value = token
}

return scopedConfig
}

Expand Down Expand Up @@ -255,7 +266,23 @@ func LocalConfig(cmd *cobra.Command) models.ScopedOptions {

// AllConfigs get all configs we know about
func AllConfigs() map[string]models.FileScopedOptions {
return configContents.Scoped
all := map[string]models.FileScopedOptions{}
for scope, scopedOptions := range configContents.Scoped {
options := scopedOptions

if controllers.IsKeyringSecret(options.Token) {
utils.LogDebug(fmt.Sprintf("Retrieving %s from system keyring", models.ConfigToken.String()))
token, err := controllers.GetKeyring(options.Token)
if !err.IsNil() {
utils.HandleError(err.Unwrap(), err.Message)
}

options.Token = token
}

all[scope] = options
}
return all
}

// Set properties on a scoped config
Expand All @@ -266,12 +293,39 @@ func Set(scope string, options map[string]string) {
utils.HandleError(err, fmt.Sprintf("Invalid scope: %s", scope))
}

config := configContents.Scoped[normalizedScope]
previousToken := config.Token

for key, value := range options {
if !IsValidConfigOption(key) {
utils.HandleError(errors.New("invalid option "+key), "")
}

config := configContents.Scoped[normalizedScope]
if key == models.ConfigToken.String() {
utils.LogDebug(fmt.Sprintf("Saving %s to system keyring", key))
uuid, err := utils.UUID()
if err != nil {
utils.HandleError(err, "Unable to generate UUID for keyring")
}
id := controllers.GenerateKeyringID(uuid)

if controllerError := controllers.SetKeyring(id, value); !controllerError.IsNil() {
utils.LogDebugError(controllerError.Unwrap())
utils.LogDebug(controllerError.Message)
} else {
value = id

utils.LogDebug("Removing previous token from system keyring")
// remove old token from keyring
if controllers.IsKeyringSecret(previousToken) {
if controllerError := controllers.DeleteKeyring(previousToken); !controllerError.IsNil() {
utils.LogDebugError(controllerError.Unwrap())
utils.LogDebug(controllerError.Message)
}
}
}
}

SetConfigValue(&config, key, value)
configContents.Scoped[normalizedScope] = config
}
Expand All @@ -297,6 +351,18 @@ func Unset(scope string, options []string) {
}

config := configContents.Scoped[normalizedScope]

if key == models.ConfigToken.String() {
previousToken := config.Token
// remove old token from keyring
if controllers.IsKeyringSecret(previousToken) {
if controllerError := controllers.DeleteKeyring(previousToken); !controllerError.IsNil() {
utils.LogDebugError(controllerError.Unwrap())
utils.LogDebug(controllerError.Message)
}
}
}

SetConfigValue(&config, key, "")
configContents.Scoped[normalizedScope] = config
}
Expand Down
74 changes: 74 additions & 0 deletions pkg/controllers/keyring.go
@@ -0,0 +1,74 @@
/*
Copyright © 2020 Doppler <support@doppler.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controllers

import (
"fmt"
"strings"

"github.com/zalando/go-keyring"
)

const keyringService = "doppler-cli"
const keyringSecretPrefix = "secret"

// IsKeyringSecret checks whether the secret is stored in keyring
func IsKeyringSecret(value string) bool {
return strings.HasPrefix(value, keyringSecretPrefix)
}

// GenerateKeyringID generates a keyring-compliant key
func GenerateKeyringID(id string) string {
return fmt.Sprintf("%s-%s", keyringSecretPrefix, id)
}

// GetKeyring fetches a secret from the keyring
func GetKeyring(id string) (string, Error) {
value, err := keyring.Get(keyringService, id)
if err != nil {
if err == keyring.ErrUnsupportedPlatform {
return "", Error{Err: err, Message: "Your OS does not support keyring"}
} else if err == keyring.ErrNotFound {
return "", Error{Err: err, Message: "Token not found in system keyring"}
} else {
return "", Error{Err: err, Message: "Unable to retrieve value from system keyring"}
}
}

return value, Error{}
}

// SetKeyring saves a value to the keyring
func SetKeyring(key string, value string) Error {
if err := keyring.Set(keyringService, key, value); err != nil {
if err == keyring.ErrUnsupportedPlatform {
return Error{Err: err, Message: "Your OS does not support keyring"}
} else {
return Error{Err: err, Message: "Unable to access system keyring for secure storage"}
}
}

return Error{}
}

// DeleteKeyring removes a value from the keyring
func DeleteKeyring(key string) Error {
if err := keyring.Delete(keyringService, key); err != nil {
return Error{Err: err, Message: "Unable to remove value from keyring"}
}

return Error{}
}
12 changes: 12 additions & 0 deletions pkg/utils/util.go
Expand Up @@ -32,6 +32,7 @@ import (
"github.com/AlecAivazis/survey/v2"
"github.com/AlecAivazis/survey/v2/terminal"
"github.com/atotto/clipboard"
"github.com/google/uuid"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -372,3 +373,14 @@ func HostArch() string {
func IsWindows() bool {
return runtime.GOOS == "windows"
}

// UUID generates a random UUID
func UUID() (string, error) {
uuid, err := uuid.NewRandom()
if err != nil {
LogDebug("Unable to generate random UUID")
return "", err
}

return uuid.String(), nil
}

0 comments on commit 95b8b50

Please sign in to comment.