Skip to content

Commit

Permalink
chore(secrets): Abstract secret implementation (#13953)
Browse files Browse the repository at this point in the history
(cherry picked from commit e2c4e10)
  • Loading branch information
srebhan authored and powersj committed Oct 2, 2023
1 parent 16b44b6 commit 279ddee
Show file tree
Hide file tree
Showing 52 changed files with 484 additions and 358 deletions.
5 changes: 2 additions & 3 deletions cmd/telegraf/cmd_secretstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@ import (
"sort"
"strings"

"github.com/awnumar/memguard"
"github.com/urfave/cli/v2"
"golang.org/x/term"

"github.com/influxdata/telegraf/config"
)

func processFilterOnlySecretStoreFlags(ctx *cli.Context) Filters {
Expand Down Expand Up @@ -119,7 +118,7 @@ To also reveal the actual secret, i.e. the value, you can pass the
}
}
_, _ = fmt.Printf(" %-30s %s\n", k, string(v))
config.ReleaseSecret(v)
memguard.WipeBytes(v)
}
}

Expand Down
127 changes: 79 additions & 48 deletions config/secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import (
"strings"
"sync/atomic"

"github.com/awnumar/memguard"

"github.com/influxdata/telegraf"
)

Expand All @@ -26,17 +24,67 @@ var secretStorePattern = regexp.MustCompile(`^\w+$`)
// in a secret-store.
var secretPattern = regexp.MustCompile(`@\{(\w+:\w+)\}`)

// secretCount is the number of secrets use in Telegraf
var secretCount atomic.Int64

// selectedImpl is the configured implementation for secrets
var selectedImpl secretImpl = &protectedSecretImpl{}

// secretImpl represents an abstraction for different implementations of secrets
type secretImpl interface {
Container(secret []byte) secretContainer
EmptyBuffer() SecretBuffer
Wipe(secret []byte)
}

// secretContainer represents an abstraction of the container holding the
// actual secret value
type secretContainer interface {
Destroy()
Equals(ref []byte) (bool, error)
Buffer() (SecretBuffer, error)
AsBuffer(secret []byte) SecretBuffer
Replace(secret []byte)
}

// SecretBuffer allows to access the content of the secret
type SecretBuffer interface {
// Size returns the length of the buffer content
Size() int
// Grow will grow the capacity of the underlying buffer to the given size
Grow(capacity int)
// Bytes returns the content of the buffer as bytes.
// NOTE: The returned bytes shall NOT be accessed after destroying the
// buffer using 'Destroy()' as the underlying the memory area might be
// wiped and invalid.
Bytes() []byte
// TemporaryString returns the content of the buffer as a string.
// NOTE: The returned String shall NOT be accessed after destroying the
// buffer using 'Destroy()' as the underlying the memory area might be
// wiped and invalid.
TemporaryString() string
// String returns a copy of the underlying buffer's content as string.
// It is safe to use the returned value after destroying the buffer.
String() string
// Destroy will wipe the buffer's content and destroy the underlying
// buffer. Do not access the buffer after destroying it.
Destroy()
}

// Secret safely stores sensitive data such as a password or token
type Secret struct {
enclave *memguard.Enclave
// container is the implementation for holding the secret. It can be
// protected or not depending on the concrete implementation.
container secretContainer

// resolvers are the functions for resolving a given secret-id (key)
resolvers map[string]telegraf.ResolveFunc

// unlinked contains all references in the secret that are not yet
// linked to the corresponding secret store.
unlinked []string

// Denotes if the secret is completely empty
// notempty denotes if the secret is completely empty
notempty bool
}

Expand Down Expand Up @@ -71,10 +119,10 @@ func (s *Secret) init(secret []byte) {

// Find all parts that need to be resolved and return them
s.unlinked = secretPattern.FindAllString(string(secret), -1)

// Setup the enclave
s.enclave = memguard.NewEnclave(secret)
s.resolvers = nil

// Setup the container implementation
s.container = selectedImpl.Container(secret)
}

// Destroy the secret content
Expand All @@ -83,16 +131,10 @@ func (s *Secret) Destroy() {
s.unlinked = nil
s.notempty = false

if s.enclave == nil {
return
}

// Wipe the secret from memory
lockbuf, err := s.enclave.Open()
if err == nil {
lockbuf.Destroy()
if s.container != nil {
s.container.Destroy()
s.container = nil
}
s.enclave = nil

// Keep track of the number of secrets...
secretCount.Add(-1)
Expand All @@ -105,51 +147,41 @@ func (s *Secret) Empty() bool {

// EqualTo performs a constant-time comparison of the secret to the given reference
func (s *Secret) EqualTo(ref []byte) (bool, error) {
if s.enclave == nil {
if s.container == nil {
return false, nil
}

if len(s.unlinked) > 0 {
return false, fmt.Errorf("unlinked parts in secret: %v", strings.Join(s.unlinked, ";"))
}

// Get a locked-buffer of the secret to perform the comparison
lockbuf, err := s.enclave.Open()
if err != nil {
return false, fmt.Errorf("opening enclave failed: %w", err)
}
defer lockbuf.Destroy()

return lockbuf.EqualTo(ref), nil
return s.container.Equals(ref)
}

// Get return the string representation of the secret
func (s *Secret) Get() ([]byte, error) {
if s.enclave == nil {
return nil, nil
func (s *Secret) Get() (SecretBuffer, error) {
if s.container == nil {
return selectedImpl.EmptyBuffer(), nil
}

if len(s.unlinked) > 0 {
return nil, fmt.Errorf("unlinked parts in secret: %v", strings.Join(s.unlinked, ";"))
}

// Decrypt the secret so we can return it
lockbuf, err := s.enclave.Open()
buffer, err := s.container.Buffer()
if err != nil {
return nil, fmt.Errorf("opening enclave failed: %w", err)
return nil, err
}
defer lockbuf.Destroy()
secret := lockbuf.Bytes()

// We've got a static secret so simply return the buffer
if len(s.resolvers) == 0 {
// Make a copy as we cannot access lockbuf after Destroy, i.e.
// after this function finishes.
newsecret := append([]byte{}, secret...)
return newsecret, protect(newsecret)
return buffer, nil
}
defer buffer.Destroy()

replaceErrs := make([]string, 0)
newsecret := secretPattern.ReplaceAllFunc(secret, func(match []byte) []byte {
newsecret := secretPattern.ReplaceAllFunc(buffer.Bytes(), func(match []byte) []byte {
resolver, found := s.resolvers[string(match)]
if !found {
replaceErrs = append(replaceErrs, fmt.Sprintf("no resolver for %q", match))
Expand All @@ -164,11 +196,11 @@ func (s *Secret) Get() ([]byte, error) {
return replacement
})
if len(replaceErrs) > 0 {
memguard.WipeBytes(newsecret)
selectedImpl.Wipe(newsecret)
return nil, fmt.Errorf("replacing secrets failed: %s", strings.Join(replaceErrs, ";"))
}

return newsecret, protect(newsecret)
return s.container.AsBuffer(newsecret), nil
}

// Set overwrites the secret's value with a new one. Please note, the secret
Expand All @@ -182,7 +214,7 @@ func (s *Secret) Set(value []byte) error {
}

// Set the new secret
s.enclave = memguard.NewEnclave(secret)
s.container.Replace(secret)
s.resolvers = res
s.notempty = len(value) > 0

Expand All @@ -198,27 +230,26 @@ func (s *Secret) GetUnlinked() []string {
// secret-store resolvers.
func (s *Secret) Link(resolvers map[string]telegraf.ResolveFunc) error {
// Decrypt the secret so we can return it
if s.enclave == nil {
if s.container == nil {
return nil
}
lockbuf, err := s.enclave.Open()
buffer, err := s.container.Buffer()
if err != nil {
return fmt.Errorf("opening enclave failed: %w", err)
return err
}
defer lockbuf.Destroy()
secret := lockbuf.Bytes()
defer buffer.Destroy()

// Iterate through the parts and try to resolve them. For static parts
// we directly replace them, while for dynamic ones we store the resolver.
newsecret, res, replaceErrs := resolve(secret, resolvers)
newsecret, res, replaceErrs := resolve(buffer.Bytes(), resolvers)
if len(replaceErrs) > 0 {
return fmt.Errorf("linking secrets failed: %s", strings.Join(replaceErrs, ";"))
}
s.resolvers = res

// Store the secret if it has changed
if string(secret) != string(newsecret) {
s.enclave = memguard.NewEnclave(newsecret)
if buffer.TemporaryString() != string(newsecret) {
s.container.Replace(newsecret)
}

// All linked now
Expand Down
131 changes: 131 additions & 0 deletions config/secret_protected.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package config

import (
"fmt"

"github.com/awnumar/memguard"
)

type protectedSecretImpl struct{}

func (*protectedSecretImpl) Container(secret []byte) secretContainer {
return &protectedSecretContainer{
enclave: memguard.NewEnclave(secret),
}
}

func (*protectedSecretImpl) EmptyBuffer() SecretBuffer {
return &lockedBuffer{}
}

func (*protectedSecretImpl) Wipe(secret []byte) {
memguard.WipeBytes(secret)
}

type lockedBuffer struct {
buf *memguard.LockedBuffer
}

func (lb *lockedBuffer) Size() int {
if lb.buf == nil {
return 0
}
return lb.buf.Size()
}

func (lb *lockedBuffer) Grow(capacity int) {
size := lb.Size()
if capacity <= size {
return
}

buf := memguard.NewBuffer(capacity)
if lb.buf != nil {
buf.Copy(lb.buf.Bytes())
}
lb.buf.Destroy()
lb.buf = buf
}

func (lb *lockedBuffer) Bytes() []byte {
if lb.buf == nil {
return nil
}
return lb.buf.Bytes()
}

func (lb *lockedBuffer) TemporaryString() string {
if lb.buf == nil {
return ""
}
return lb.buf.String()
}

func (lb *lockedBuffer) String() string {
if lb.buf == nil {
return ""
}
return string(lb.buf.Bytes())
}

func (lb *lockedBuffer) Destroy() {
if lb.buf == nil {
return
}
lb.buf.Destroy()
lb.buf = nil
}

type protectedSecretContainer struct {
enclave *memguard.Enclave
}

func (c *protectedSecretContainer) Destroy() {
if c.enclave == nil {
return
}

// Wipe the secret from memory
lockbuf, err := c.enclave.Open()
if err == nil {
lockbuf.Destroy()
}
c.enclave = nil
}

func (c *protectedSecretContainer) Equals(ref []byte) (bool, error) {
if c.enclave == nil {
return false, nil
}

// Get a locked-buffer of the secret to perform the comparison
lockbuf, err := c.enclave.Open()
if err != nil {
return false, fmt.Errorf("opening enclave failed: %w", err)
}
defer lockbuf.Destroy()

return lockbuf.EqualTo(ref), nil
}

func (c *protectedSecretContainer) Buffer() (SecretBuffer, error) {
if c.enclave == nil {
return &lockedBuffer{}, nil
}

// Get a locked-buffer of the secret to perform the comparison
lockbuf, err := c.enclave.Open()
if err != nil {
return nil, fmt.Errorf("opening enclave failed: %w", err)
}

return &lockedBuffer{lockbuf}, nil
}

func (c *protectedSecretContainer) AsBuffer(secret []byte) SecretBuffer {
return &lockedBuffer{memguard.NewBufferFromBytes(secret)}
}

func (c *protectedSecretContainer) Replace(secret []byte) {
c.enclave = memguard.NewEnclave(secret)
}
Loading

0 comments on commit 279ddee

Please sign in to comment.