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

Commit

Permalink
Merge pull request #9 from puppetlabs/tasks/add-secret-value-encoding…
Browse files Browse the repository at this point in the history
…-interface

New: Add secrets/encoding package to standardize secret value encoding for storage
  • Loading branch information
kyleterry authored Aug 14, 2019
2 parents 73edc1a + 6619827 commit 43cc118
Show file tree
Hide file tree
Showing 5 changed files with 294 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

## encoding/transfer

This package provides an interface to encode and decode values that will get stored
in a data 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 or outputs.
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/decoded.

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

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

// encodingTypeMap is an internal map used to get the encodingType type from a string
var encodingTypeMap = map[string]encodingType{
"base64": Base64EncodingType,
}

// 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.SplitN(value, ":", 2)

if len(parts) < 2 {
return NoEncodingType, value
}

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

return t, parts[1]
}

// Encoders maps encoding algorithms to their respective EncodeDecoder types.
// Example:
//
// ed := transfer.Encoders[Base64EncodingType]()
// encodedValue, err := ed.EncodeForTransfer("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 values using base64.
// All encoded values will be prefixed with "base64:"
type Base64Encoding struct{}

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

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

// DecodeFromTransfer 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) DecodeFromTransfer(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{}

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

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

// EncodeForTransfer uses the DefaultEncodingType to encode value.
func EncodeForTransfer(value []byte) (string, error) {
encoder := Encoders[DefaultEncodingType]()

return encoder.EncodeForTransfer(value)
}

// DecodeFromTransfer uses ParseEncodedValue to find the right encoder then
// decodes value with it.
func DecodeFromTransfer(value string) ([]byte, error) {
t, val := ParseEncodedValue(value)
encoder := Encoders[t]()

return encoder.DecodeFromTransfer(val)
}
133 changes: 133 additions & 0 deletions encoding/transfer/encoding_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package transfer

import (
"encoding/base64"
"fmt"
"testing"

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

func TestEncoding(t *testing.T) {
var cases = []struct {
description string
value string
encodingType encodingType
expected string
customResultTest func(t *testing.T, encoded string, decoded []byte)
}{
{
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",
},
{
description: "begins with :",
value: ":fun time at the park",
encodingType: NoEncodingType,
expected: ":fun time at the park",
},
{
description: "user encoded base64",
value: "c3VwZXIgc2VjcmV0IHRva2Vu",
encodingType: NoEncodingType,
expected: "c3VwZXIgc2VjcmV0IHRva2Vu",
},
{
description: "user encoded base64 wrapped with our base64 encoder",
// "super secret token" encoded as base64
value: "c3VwZXIgc2VjcmV0IHRva2Vu",
encodingType: Base64EncodingType,
expected: "base64:YzNWd1pYSWdjMlZqY21WMElIUnZhMlZ1",
customResultTest: func(t *testing.T, encoded string, decoded []byte) {
// tests that a user can encode their own values, have our system wrap it in our own
// encoding, then when they try to unwrap their encoding, they get the expected value.
result, err := base64.StdEncoding.DecodeString(string(decoded))
require.NoError(t, err)

require.Equal(t, "super secret token", string(result))
},
},
}

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

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

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

newED := Encoders[typ]()

var decoded []byte

decoded, err = newED.DecodeFromTransfer(value)
require.NoError(t, err)
require.Equal(t, c.value, string(decoded))

if c.customResultTest != nil {
t.Run("custom result test", func(t *testing.T) {
c.customResultTest(t, encoded, decoded)
})
}
})
}
}

func TestHelperFuncs(t *testing.T) {
var cases = []struct {
description string
value string
expected string
}{
{
description: "base64 encoding succeeds",
value: "super secret token",
expected: "base64:c3VwZXIgc2VjcmV0IHRva2Vu",
},
{
description: "user encoded base64 wrapped with our base64 encoder",
// "super secret token" encoded as base64
value: "c3VwZXIgc2VjcmV0IHRva2Vu",
expected: "base64:YzNWd1pYSWdjMlZqY21WMElIUnZhMlZ1",
},
}

for _, c := range cases {
t.Run(c.description, func(t *testing.T) {
encoded, err := EncodeForTransfer([]byte(c.value))
require.NoError(t, err)

require.Equal(t, c.expected, encoded)

var decoded []byte

decoded, err = DecodeFromTransfer(encoded)
require.NoError(t, err)
require.Equal(t, c.value, string(decoded))
})
}
}
17 changes: 17 additions & 0 deletions encoding/transfer/interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package transfer

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

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

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

0 comments on commit 43cc118

Please sign in to comment.