diff --git a/pkg/cmd/decode.go b/pkg/cmd/decode.go new file mode 100644 index 0000000..3a99af8 --- /dev/null +++ b/pkg/cmd/decode.go @@ -0,0 +1,43 @@ +package cmd + +import ( + "compress/gzip" + "encoding/base64" + "fmt" + "io" + "strings" +) + +type SecretDecoder interface { + Decode(input string) (string, error) +} + +func (s Secret) Decode(input string) (string, error) { + switch s.Type { + // TODO handle all secret types + case Opaque: + b64d, err := base64.StdEncoding.DecodeString(input) + if err != nil { + return "", nil + } + return string(b64d), nil + case Helm: + b64dk8s, _ := base64.StdEncoding.DecodeString(input) + b64dhelm, _ := base64.StdEncoding.DecodeString(string(b64dk8s)) + + gz, err := gzip.NewReader(strings.NewReader(string(b64dhelm))) + if err != nil { + return "", err + } + defer gz.Close() + + s, err := io.ReadAll(gz) + if err != nil { + return "", err + } + + return string(s), nil + } + + return "", fmt.Errorf("couldn't decode unknown secret type %q", s.Type) +} diff --git a/pkg/cmd/decode_test.go b/pkg/cmd/decode_test.go new file mode 100644 index 0000000..d81e365 --- /dev/null +++ b/pkg/cmd/decode_test.go @@ -0,0 +1,72 @@ +package cmd + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDecode(t *testing.T) { + tests := map[string]struct { + data func() Secret + want string + }{ + // base64 encoded + "opaque": { + func() Secret { + return Secret{ + Data: map[string]string{ + "key": "dGVzdAo=", + }, + Type: Opaque, + } + }, + "test\n", + }, + // double base64 encoded + gzip'd + "helm": { + func() Secret { + res := Secret{ + Type: Helm, + } + var buf bytes.Buffer + gz := gzip.NewWriter(&buf) + if _, err := gz.Write([]byte("test\n")); err != nil { + return res + } + if err := gz.Close(); err != nil { + return res + } + + b64k8s := base64.StdEncoding.EncodeToString(buf.Bytes()) + b64helm := base64.StdEncoding.EncodeToString([]byte(b64k8s)) + + res.Data = SecretData{ + "key": b64helm, + } + + return res + }, + "test\n", + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + data := tt.data() + want := tt.want + + got, err := data.Decode(data.Data["key"]) + if err != nil { + t.Errorf("got %v, want %v", got, want) + } + + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/cmd/types.go b/pkg/cmd/types.go index 4eecb8d..37f4d00 100644 --- a/pkg/cmd/types.go +++ b/pkg/cmd/types.go @@ -7,6 +7,7 @@ type SecretList struct { type Secret struct { Data SecretData `json:"data"` Metadata Metadata `json:"metadata"` + Type SecretType `json:"type"` } type SecretData map[string]string @@ -15,3 +16,32 @@ type Metadata struct { Name string `json:"name"` Namespace string `json:"namespace"` } + +// SecretType represents the type of a secret +// +// Opaque arbitrary user-defined data +// kubernetes.io/service-account-token ServiceAccount token +// kubernetes.io/dockercfg serialized ~/.dockercfg file +// kubernetes.io/dockerconfigjson serialized ~/.docker/config.json file +// kubernetes.io/basic-auth credentials for basic authentication +// kubernetes.io/ssh-auth credentials for SSH authentication +// kubernetes.io/tls data for a TLS client or server +// bootstrap.kubernetes.io/token bootstrap token data +// helm.sh/release.v1 Helm v3 release data +// +// refs: +// - https://kubernetes.io/docs/concepts/configuration/secret/#secret-types +// - https://gist.github.com/DzeryCZ/c4adf39d4a1a99ae6e594a183628eaee +type SecretType string + +const ( + BasicAuth SecretType = "kubernetes.io/basic-auth" + DockerCfg SecretType = "kubernetes.io/dockercfg" + DockerConfigJson SecretType = "kubernetes.io/dockerconfigjson" + Helm SecretType = "helm.sh/release.v1" + Opaque SecretType = "Opaque" + ServiceAccountToken SecretType = "kubernetes.io/service-account-token" + SshAuth SecretType = "kubernetes.io/ssh-auth" + Tls SecretType = "kubernetes.io/tls" + Token SecretType = "bootstrap.kubernetes.io/token" +) diff --git a/pkg/cmd/types_test.go b/pkg/cmd/types_test.go index 508bc29..dbbdfb0 100644 --- a/pkg/cmd/types_test.go +++ b/pkg/cmd/types_test.go @@ -27,7 +27,20 @@ var ( "type": "Opaque" }` - emptySecretJson = `{ + helmSecretJson = `{ + "apiVersion": "v1", + "data": { + "release": "blob" + }, + "kind": "Secret", + "metadata": { + "name": "sh.helm.release.v1.wordpress.v1", + "namespace": "default" + }, + "type": "helm.sh/release.v1" + }` + + invalidSecretJson = `{ "apiVersion": "v1", "data": {}, "kind": "Secret", @@ -45,17 +58,18 @@ func TestSerialize(t *testing.T) { want Secret wantErr error }{ - "empty secret": { - input: emptySecretJson, + "empty opqague secret": { + input: invalidSecretJson, want: Secret{ Metadata: Metadata{ Name: "test", Namespace: "default", }, + Type: Opaque, }, wantErr: errors.New("invalid character '}' looking for beginning of object key string"), }, - "valid secret": { + "valid opague secret": { input: validSecretJson, want: Secret{ Data: SecretData{ @@ -66,6 +80,20 @@ func TestSerialize(t *testing.T) { Name: "test", Namespace: "default", }, + Type: Opaque, + }, + }, + "valid helm secret": { + input: helmSecretJson, + want: Secret{ + Data: SecretData{ + "release": "blob", + }, + Metadata: Metadata{ + Name: "sh.helm.release.v1.wordpress.v1", + Namespace: "default", + }, + Type: Helm, }, }, } diff --git a/pkg/cmd/view-secret.go b/pkg/cmd/view-secret.go index 8cbd9b5..2d57375 100644 --- a/pkg/cmd/view-secret.go +++ b/pkg/cmd/view-secret.go @@ -2,7 +2,6 @@ package cmd import ( "bytes" - "encoding/base64" "errors" "fmt" "io" @@ -221,19 +220,19 @@ func ProcessSecret(outWriter, errWriter io.Writer, inputReader io.Reader, secret if decodeAll { for _, k := range keys { - b64d, _ := base64.StdEncoding.DecodeString(data[k]) - _, _ = fmt.Fprintf(outWriter, "%s='%s'\n", k, string(b64d)) + s, _ := secret.Decode(data[k]) + _, _ = fmt.Fprintf(outWriter, "%s='%s'\n", k, s) } } else if len(data) == 1 { for k, v := range data { _, _ = fmt.Fprintf(errWriter, singleKeyDescription+"\n", k) - b64d, _ := base64.StdEncoding.DecodeString(v) - _, _ = fmt.Fprintf(outWriter, "%s\n", string(b64d)) + s, _ := secret.Decode(v) + _, _ = fmt.Fprintf(outWriter, "%s\n", s) } } else if secretKey != "" { if v, ok := data[secretKey]; ok { - b64d, _ := base64.StdEncoding.DecodeString(v) - _, _ = fmt.Fprintf(outWriter, "%s\n", string(b64d)) + s, _ := secret.Decode(v) + _, _ = fmt.Fprintf(outWriter, "%s\n", s) } else { return ErrSecretKeyNotFound } diff --git a/pkg/cmd/view-secret_test.go b/pkg/cmd/view-secret_test.go index 28403cf..669fdab 100644 --- a/pkg/cmd/view-secret_test.go +++ b/pkg/cmd/view-secret_test.go @@ -170,7 +170,7 @@ func TestProcessSecret(t *testing.T) { readBuf = *strings.NewReader(test.feedkeys) } - err := ProcessSecret(&stdOutBuf, &stdErrBuf, &readBuf, Secret{Data: test.secretData}, test.secretKey, test.decodeAll) + err := ProcessSecret(&stdOutBuf, &stdErrBuf, &readBuf, Secret{Data: test.secretData, Type: Opaque}, test.secretKey, test.decodeAll) if test.err != nil { assert.Equal(t, err, test.err)