Skip to content
This repository has been archived by the owner on Dec 8, 2020. It is now read-only.

Commit

Permalink
New: Add secrets/encoding package to standardize secret value encodin…
Browse files Browse the repository at this point in the history
…g for storage

To maintain secret value integrity, we need to encode the value bytes in a common encoding for
storage. We also need to make it transparent to the user so we don't force them to encode their
values before adding them to the secret store. This package also helps maintain backwards
compatibility with the current system that doesn't have any consistent value encoding.
  • Loading branch information
kyleterry committed Aug 13, 2019
1 parent 73edc1a commit d188a7b
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 0 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,14 @@ The functionality in this package should work on Linux, MacOS and the BSDs.

- add a mechanism for root interactions
- add Windows support

## secrets/encoding

This package provides an interface to encode and decode secret values that will get stored
in the secret store. This is required to ensure that values are consistently stored safely,
but also doesn't enforce an encoding that users must use when storing secrets. The default
algorithm is base64 and all encoded strings generated will be prefixed with "base64:". If
there is no encoding prefix, there is `NoEncodingType` that will just return the original
value unencoded.

Help can be found by running `go doc -all github.com/puppetlabs/horsehead/secrets/encoding`.
23 changes: 23 additions & 0 deletions secrets/encoding/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
Package encoding provides an interface for encoding and decoding secret
values for storage. The utility in this package is transparent to the user
and it is used to maintain byte integrity on secret values used in workflows.
Basic use when encoding a value:
encoder := encoding.Encoders[encoding.DefaultEncodingType]()
result, err := encoder.EncodeSecretValue([]byte("super secret token"))
if err != nil {
// handle error
}
Basic use when decoding a value:
encodingType, value := encoding.ParseEncodedValue("base64:c3VwZXIgc2VjcmV0IHRva2Vu")
encoder := encoding.Encoders[encoderType]()
result, err := encoder.DecodeSecretValue(value)
if err != nil {
// handle error
}
*/
package encoding
90 changes: 90 additions & 0 deletions secrets/encoding/encoding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package encoding

import (
"encoding/base64"
"fmt"
"strings"
)

type encodingType string

func (p encodingType) String() string {
return string(p)
}

const (
Base64EncodingType encodingType = "base64"
NoEncodingType encodingType = ""
)

// DefaultEncodingType is the default encodingType. This makes it easier to use this
// package as the caller won't need to make any desisions around what encoder to use
// unless they really need to.
const DefaultEncodingType = Base64EncodingType

var encodingTypeMap = map[string]encodingType{
"base64": Base64EncodingType,
"": NoEncodingType,
}

// ParseEncodedValue will attempt to split on : and extract an encoding identifer
// from the prefix of the string. It then returns the discovered encodingType and the
// value without the encodingType prefixed.
func ParseEncodedValue(value string) (encodingType, string) {
parts := strings.Split(value, ":")

t, ok := encodingTypeMap[parts[0]]
if !ok {
return NoEncodingType, value
}

return t, strings.Join(parts[1:], ":")
}

// Encoders maps encoding algorithms to their respective EncodeDecoder types.
// Example:
//
// ed := encoding.Encoders[Base64EncodingType]()
// encodedValue, err := ed.EncodeSecretValue("my super secret value")
var Encoders = map[encodingType]func() EncodeDecoder{
Base64EncodingType: func() EncodeDecoder {
return Base64Encoding{}
},
NoEncodingType: func() EncodeDecoder {
return NoEncoding{}
},
}

// Base64Encoding handles the encoding and decoding of secret values using base64.
// All encoded values will be prefixed with "base64:"
type Base64Encoding struct{}

// EncodeSecretValue takes a byte slice and returns it encoded as a base64 string.
// No error is ever returned.
func (e Base64Encoding) EncodeSecretValue(value []byte) (string, error) {
s := base64.StdEncoding.EncodeToString(value)

return fmt.Sprintf("%s:%s", Base64EncodingType, s), nil
}

// DecodeSecretValue takes a string and attempts to decode using a base64 decoder.
// If an error is returned, it will originate from the Go encoding/base64 package.
func (e Base64Encoding) DecodeSecretValue(value string) ([]byte, error) {
return base64.StdEncoding.DecodeString(value)
}

// NoEncoding just returns the values without encoding them. This is used when there
// is no encoding type algorithm prefix on the value.
type NoEncoding struct{}

// EncodeSecretValue takes a byte slice and casts it to a string. No error is ever
// returned.
func (e NoEncoding) EncodeSecretValue(value []byte) (string, error) {
return string(value), nil
}

// DecodeSecretValue takes a string and casts it to a byte slice. No error is ever
// returned.
func (e NoEncoding) DecodeSecretValue(value string) ([]byte, error) {
return []byte(value), nil
}
63 changes: 63 additions & 0 deletions secrets/encoding/encoding_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package encoding

import (
"fmt"
"testing"

"github.com/stretchr/testify/require"
)

func TestEncoding(t *testing.T) {
var cases = []struct {
description string
value string
encodingType encodingType
expected string
}{
{
description: "base64 encoding succeeds",
value: "super secret token",
encodingType: Base64EncodingType,
expected: "base64:c3VwZXIgc2VjcmV0IHRva2Vu",
},
{
description: "no encoding succeeds",
value: "super secret token",
encodingType: NoEncodingType,
expected: "super secret token",
},
{
description: "base64 complex values don't loose integrity",
value: "super: secret token:12:49:wheel",
encodingType: Base64EncodingType,
expected: "base64:c3VwZXI6IHNlY3JldCB0b2tlbjoxMjo0OTp3aGVlbA==",
},
{
description: "no encoding complex values don't loose integrity",
value: "super: secret token:12:49:wheel",
encodingType: NoEncodingType,
expected: "super: secret token:12:49:wheel",
},
}

for _, c := range cases {
t.Run(c.description, func(t *testing.T) {
ed := Encoders[c.encodingType]()

result, err := ed.EncodeSecretValue([]byte(c.value))
require.NoError(t, err)
require.Equal(t, c.expected, result, fmt.Sprintf("result was malformed: %s", result))

typ, value := ParseEncodedValue(result)
require.Equal(t, c.encodingType, typ)

newED := Encoders[typ]()

{
newResult, err := newED.DecodeSecretValue(value)
require.NoError(t, err)
require.Equal(t, c.value, string(newResult))
}
})
}
}
18 changes: 18 additions & 0 deletions secrets/encoding/interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package encoding

// Encoder encodes a byte slice and returns a string with the encoding type prefixed
type Encoder interface {
EncodeSecretValue([]byte) (string, error)
}

// Decoder takes a string and decodes it, returning a byte slice or an error
type Decoder interface {
DecodeSecretValue(string) ([]byte, error)
}

// EncodeDecoder groups Encoder and Decoder to form a type that can both encode and decode
// secret values.
type EncodeDecoder interface {
Encoder
Decoder
}

0 comments on commit d188a7b

Please sign in to comment.