github.com/hashicorp/vault/sdk@v0.11.0/helper/keysutil/encrypted_key_storage.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package keysutil
     5  
     6  import (
     7  	"context"
     8  	"encoding/base64"
     9  	"errors"
    10  	"math/big"
    11  	paths "path"
    12  	"sort"
    13  	"strings"
    14  
    15  	lru "github.com/hashicorp/golang-lru"
    16  	"github.com/hashicorp/vault/sdk/logical"
    17  )
    18  
    19  const (
    20  	// DefaultCacheSize is used if no cache size is specified for
    21  	// NewEncryptedKeyStorage. This value is the number of cache entries to
    22  	// store, not the size in bytes of the cache.
    23  	DefaultCacheSize = 16 * 1024
    24  
    25  	// DefaultPrefix is used if no prefix is specified for
    26  	// NewEncryptedKeyStorage. Prefix must be defined so we can provide context
    27  	// for the base folder.
    28  	DefaultPrefix = "encryptedkeys/"
    29  
    30  	// EncryptedKeyPolicyVersionTpl is a template that can be used to minimize
    31  	// the amount of data that's stored with the ciphertext.
    32  	EncryptedKeyPolicyVersionTpl = "{{version}}:"
    33  )
    34  
    35  var (
    36  	// ErrPolicyDerivedKeys is returned if the provided policy does not use
    37  	// derived keys. This is a requirement for this storage implementation.
    38  	ErrPolicyDerivedKeys = errors.New("key policy must use derived keys")
    39  
    40  	// ErrPolicyConvergentEncryption is returned if the provided policy does not use
    41  	// convergent encryption. This is a requirement for this storage implementation.
    42  	ErrPolicyConvergentEncryption = errors.New("key policy must use convergent encryption")
    43  
    44  	// ErrPolicyConvergentVersion is returned if the provided policy does not use
    45  	// a new enough convergent version. This is a requirement for this storage
    46  	// implementation.
    47  	ErrPolicyConvergentVersion = errors.New("key policy must use convergent version > 2")
    48  
    49  	// ErrNilStorage is returned if the provided storage is nil.
    50  	ErrNilStorage = errors.New("nil storage provided")
    51  
    52  	// ErrNilPolicy is returned if the provided policy is nil.
    53  	ErrNilPolicy = errors.New("nil policy provided")
    54  )
    55  
    56  // EncryptedKeyStorageConfig is used to configure an EncryptedKeyStorage object.
    57  type EncryptedKeyStorageConfig struct {
    58  	// Policy is the key policy to use to encrypt the key paths.
    59  	Policy *Policy
    60  
    61  	// Prefix is the storage prefix for this instance of the EncryptedKeyStorage
    62  	// object. This is stored in plaintext. If not set the DefaultPrefix will be
    63  	// used.
    64  	Prefix string
    65  
    66  	// CacheSize is the number of elements to cache. If not set the
    67  	// DetaultCacheSize will be used.
    68  	CacheSize int
    69  }
    70  
    71  // NewEncryptedKeyStorageWrapper takes an EncryptedKeyStorageConfig and returns a new
    72  // EncryptedKeyStorage object.
    73  func NewEncryptedKeyStorageWrapper(config EncryptedKeyStorageConfig) (*EncryptedKeyStorageWrapper, error) {
    74  	if config.Policy == nil {
    75  		return nil, ErrNilPolicy
    76  	}
    77  
    78  	if !config.Policy.Derived {
    79  		return nil, ErrPolicyDerivedKeys
    80  	}
    81  
    82  	if !config.Policy.ConvergentEncryption {
    83  		return nil, ErrPolicyConvergentEncryption
    84  	}
    85  
    86  	if config.Prefix == "" {
    87  		config.Prefix = DefaultPrefix
    88  	}
    89  
    90  	if !strings.HasSuffix(config.Prefix, "/") {
    91  		config.Prefix += "/"
    92  	}
    93  
    94  	size := config.CacheSize
    95  	if size <= 0 {
    96  		size = DefaultCacheSize
    97  	}
    98  
    99  	cache, err := lru.New2Q(size)
   100  	if err != nil {
   101  		return nil, err
   102  	}
   103  
   104  	return &EncryptedKeyStorageWrapper{
   105  		policy: config.Policy,
   106  		prefix: config.Prefix,
   107  		lru:    cache,
   108  	}, nil
   109  }
   110  
   111  type EncryptedKeyStorageWrapper struct {
   112  	policy *Policy
   113  	lru    *lru.TwoQueueCache
   114  	prefix string
   115  }
   116  
   117  func (f *EncryptedKeyStorageWrapper) Wrap(s logical.Storage) logical.Storage {
   118  	return &encryptedKeyStorage{
   119  		policy: f.policy,
   120  		s:      s,
   121  		prefix: f.prefix,
   122  		lru:    f.lru,
   123  	}
   124  }
   125  
   126  // EncryptedKeyStorage implements the logical.Storage interface and ensures the
   127  // storage paths are encrypted in the underlying storage.
   128  type encryptedKeyStorage struct {
   129  	policy *Policy
   130  	s      logical.Storage
   131  	lru    *lru.TwoQueueCache
   132  
   133  	prefix string
   134  }
   135  
   136  func ensureTailingSlash(path string) string {
   137  	if !strings.HasSuffix(path, "/") {
   138  		return path + "/"
   139  	}
   140  	return path
   141  }
   142  
   143  // List implements the logical.Storage List method, and decrypts all the items
   144  // in a path prefix. This can only operate on full folder structures so the
   145  // prefix should end in a "/".
   146  func (s *encryptedKeyStorage) List(ctx context.Context, prefix string) ([]string, error) {
   147  	var decoder big.Int
   148  
   149  	encPrefix, err := s.encryptPath(prefix)
   150  	if err != nil {
   151  		return nil, err
   152  	}
   153  
   154  	keys, err := s.s.List(ctx, ensureTailingSlash(encPrefix))
   155  	if err != nil {
   156  		return keys, err
   157  	}
   158  
   159  	decryptedKeys := make([]string, len(keys))
   160  
   161  	// The context for the decryption operations will be the object's prefix
   162  	// joined with the provided prefix. Join cleans the path ensuring there
   163  	// isn't a trailing "/".
   164  	context := []byte(paths.Join(s.prefix, prefix))
   165  
   166  	for i, k := range keys {
   167  		raw, ok := s.lru.Get(k)
   168  		if ok {
   169  			// cache HIT, we can bail early and skip the decode & decrypt operations.
   170  			decryptedKeys[i] = raw.(string)
   171  			continue
   172  		}
   173  
   174  		// If a folder is included in the keys it will have a trailing "/".
   175  		// We need to remove this before decoding/decrypting and add it back
   176  		// later.
   177  		appendSlash := strings.HasSuffix(k, "/")
   178  		if appendSlash {
   179  			k = strings.TrimSuffix(k, "/")
   180  		}
   181  
   182  		decoder.SetString(k, 62)
   183  		decoded := decoder.Bytes()
   184  		if len(decoded) == 0 {
   185  			return nil, errors.New("could not decode key")
   186  		}
   187  
   188  		// Decrypt the data with the object's key policy.
   189  		encodedPlaintext, err := s.policy.Decrypt(context, nil, string(decoded[:]))
   190  		if err != nil {
   191  			return nil, err
   192  		}
   193  
   194  		// The plaintext is still base64 encoded, decode it.
   195  		decoded, err = base64.StdEncoding.DecodeString(encodedPlaintext)
   196  		if err != nil {
   197  			return nil, err
   198  		}
   199  
   200  		plaintext := string(decoded[:])
   201  
   202  		// Add the slash back to the plaintext value
   203  		if appendSlash {
   204  			plaintext += "/"
   205  			k += "/"
   206  		}
   207  
   208  		// We want to store the unencoded version of the key in the cache.
   209  		// This will make it more performent when it's a HIT.
   210  		s.lru.Add(k, plaintext)
   211  
   212  		decryptedKeys[i] = plaintext
   213  	}
   214  
   215  	sort.Strings(decryptedKeys)
   216  	return decryptedKeys, nil
   217  }
   218  
   219  // Get implements the logical.Storage Get method.
   220  func (s *encryptedKeyStorage) Get(ctx context.Context, path string) (*logical.StorageEntry, error) {
   221  	encPath, err := s.encryptPath(path)
   222  	if err != nil {
   223  		return nil, err
   224  	}
   225  
   226  	return s.s.Get(ctx, encPath)
   227  }
   228  
   229  // Put implements the logical.Storage Put method.
   230  func (s *encryptedKeyStorage) Put(ctx context.Context, entry *logical.StorageEntry) error {
   231  	encPath, err := s.encryptPath(entry.Key)
   232  	if err != nil {
   233  		return err
   234  	}
   235  	e := &logical.StorageEntry{}
   236  	*e = *entry
   237  
   238  	e.Key = encPath
   239  
   240  	return s.s.Put(ctx, e)
   241  }
   242  
   243  // Delete implements the logical.Storage Delete method.
   244  func (s *encryptedKeyStorage) Delete(ctx context.Context, path string) error {
   245  	encPath, err := s.encryptPath(path)
   246  	if err != nil {
   247  		return err
   248  	}
   249  
   250  	return s.s.Delete(ctx, encPath)
   251  }
   252  
   253  // encryptPath takes a plaintext path and encrypts each path section (separated
   254  // by "/") with the object's key policy. The context for each encryption is the
   255  // plaintext path prefix for the key.
   256  func (s *encryptedKeyStorage) encryptPath(path string) (string, error) {
   257  	var encoder big.Int
   258  
   259  	if path == "" || path == "/" {
   260  		return s.prefix, nil
   261  	}
   262  
   263  	path = paths.Clean(path)
   264  
   265  	// Trim the prefix if it starts with a "/"
   266  	path = strings.TrimPrefix(path, "/")
   267  
   268  	parts := strings.Split(path, "/")
   269  
   270  	encPath := s.prefix
   271  	context := strings.TrimSuffix(s.prefix, "/")
   272  	for _, p := range parts {
   273  		encoded := base64.StdEncoding.EncodeToString([]byte(p))
   274  		ciphertext, err := s.policy.Encrypt(0, []byte(context), nil, encoded)
   275  		if err != nil {
   276  			return "", err
   277  		}
   278  
   279  		encoder.SetBytes([]byte(ciphertext))
   280  		encPath = paths.Join(encPath, encoder.Text(62))
   281  		context = paths.Join(context, p)
   282  	}
   283  
   284  	return encPath, nil
   285  }