// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package keysutil import ( "context" "encoding/base64" "errors" "math/big" paths "path" "sort" "strings" lru "github.com/hashicorp/golang-lru" "github.com/hashicorp/vault/sdk/logical" ) const ( // DefaultCacheSize is used if no cache size is specified for // NewEncryptedKeyStorage. This value is the number of cache entries to // store, not the size in bytes of the cache. DefaultCacheSize = 16 * 1024 // DefaultPrefix is used if no prefix is specified for // NewEncryptedKeyStorage. Prefix must be defined so we can provide context // for the base folder. DefaultPrefix = "encryptedkeys/" // EncryptedKeyPolicyVersionTpl is a template that can be used to minimize // the amount of data that's stored with the ciphertext. EncryptedKeyPolicyVersionTpl = "{{version}}:" ) var ( // ErrPolicyDerivedKeys is returned if the provided policy does not use // derived keys. This is a requirement for this storage implementation. ErrPolicyDerivedKeys = errors.New("key policy must use derived keys") // ErrPolicyConvergentEncryption is returned if the provided policy does not use // convergent encryption. This is a requirement for this storage implementation. ErrPolicyConvergentEncryption = errors.New("key policy must use convergent encryption") // ErrPolicyConvergentVersion is returned if the provided policy does not use // a new enough convergent version. This is a requirement for this storage // implementation. ErrPolicyConvergentVersion = errors.New("key policy must use convergent version > 2") // ErrNilStorage is returned if the provided storage is nil. ErrNilStorage = errors.New("nil storage provided") // ErrNilPolicy is returned if the provided policy is nil. ErrNilPolicy = errors.New("nil policy provided") ) // EncryptedKeyStorageConfig is used to configure an EncryptedKeyStorage object. type EncryptedKeyStorageConfig struct { // Policy is the key policy to use to encrypt the key paths. Policy *Policy // Prefix is the storage prefix for this instance of the EncryptedKeyStorage // object. This is stored in plaintext. If not set the DefaultPrefix will be // used. Prefix string // CacheSize is the number of elements to cache. If not set the // DetaultCacheSize will be used. CacheSize int } // NewEncryptedKeyStorageWrapper takes an EncryptedKeyStorageConfig and returns a new // EncryptedKeyStorage object. func NewEncryptedKeyStorageWrapper(config EncryptedKeyStorageConfig) (*EncryptedKeyStorageWrapper, error) { if config.Policy == nil { return nil, ErrNilPolicy } if !config.Policy.Derived { return nil, ErrPolicyDerivedKeys } if !config.Policy.ConvergentEncryption { return nil, ErrPolicyConvergentEncryption } if config.Prefix == "" { config.Prefix = DefaultPrefix } if !strings.HasSuffix(config.Prefix, "/") { config.Prefix += "/" } size := config.CacheSize if size <= 0 { size = DefaultCacheSize } cache, err := lru.New2Q(size) if err != nil { return nil, err } return &EncryptedKeyStorageWrapper{ policy: config.Policy, prefix: config.Prefix, lru: cache, }, nil } type EncryptedKeyStorageWrapper struct { policy *Policy lru *lru.TwoQueueCache prefix string } func (f *EncryptedKeyStorageWrapper) Wrap(s logical.Storage) logical.Storage { return &encryptedKeyStorage{ policy: f.policy, s: s, prefix: f.prefix, lru: f.lru, } } // EncryptedKeyStorage implements the logical.Storage interface and ensures the // storage paths are encrypted in the underlying storage. type encryptedKeyStorage struct { policy *Policy s logical.Storage lru *lru.TwoQueueCache prefix string } func ensureTailingSlash(path string) string { if !strings.HasSuffix(path, "/") { return path + "/" } return path } // List implements the logical.Storage List method, and decrypts all the items // in a path prefix. This can only operate on full folder structures so the // prefix should end in a "/". func (s *encryptedKeyStorage) List(ctx context.Context, prefix string) ([]string, error) { var decoder big.Int encPrefix, err := s.encryptPath(prefix) if err != nil { return nil, err } keys, err := s.s.List(ctx, ensureTailingSlash(encPrefix)) if err != nil { return keys, err } decryptedKeys := make([]string, len(keys)) // The context for the decryption operations will be the object's prefix // joined with the provided prefix. Join cleans the path ensuring there // isn't a trailing "/". context := []byte(paths.Join(s.prefix, prefix)) for i, k := range keys { raw, ok := s.lru.Get(k) if ok { // cache HIT, we can bail early and skip the decode & decrypt operations. decryptedKeys[i] = raw.(string) continue } // If a folder is included in the keys it will have a trailing "/". // We need to remove this before decoding/decrypting and add it back // later. appendSlash := strings.HasSuffix(k, "/") if appendSlash { k = strings.TrimSuffix(k, "/") } decoder.SetString(k, 62) decoded := decoder.Bytes() if len(decoded) == 0 { return nil, errors.New("could not decode key") } // Decrypt the data with the object's key policy. encodedPlaintext, err := s.policy.Decrypt(context, nil, string(decoded[:])) if err != nil { return nil, err } // The plaintext is still base64 encoded, decode it. decoded, err = base64.StdEncoding.DecodeString(encodedPlaintext) if err != nil { return nil, err } plaintext := string(decoded[:]) // Add the slash back to the plaintext value if appendSlash { plaintext += "/" k += "/" } // We want to store the unencoded version of the key in the cache. // This will make it more performent when it's a HIT. s.lru.Add(k, plaintext) decryptedKeys[i] = plaintext } sort.Strings(decryptedKeys) return decryptedKeys, nil } // Get implements the logical.Storage Get method. func (s *encryptedKeyStorage) Get(ctx context.Context, path string) (*logical.StorageEntry, error) { encPath, err := s.encryptPath(path) if err != nil { return nil, err } return s.s.Get(ctx, encPath) } // Put implements the logical.Storage Put method. func (s *encryptedKeyStorage) Put(ctx context.Context, entry *logical.StorageEntry) error { encPath, err := s.encryptPath(entry.Key) if err != nil { return err } e := &logical.StorageEntry{} *e = *entry e.Key = encPath return s.s.Put(ctx, e) } // Delete implements the logical.Storage Delete method. func (s *encryptedKeyStorage) Delete(ctx context.Context, path string) error { encPath, err := s.encryptPath(path) if err != nil { return err } return s.s.Delete(ctx, encPath) } // encryptPath takes a plaintext path and encrypts each path section (separated // by "/") with the object's key policy. The context for each encryption is the // plaintext path prefix for the key. func (s *encryptedKeyStorage) encryptPath(path string) (string, error) { var encoder big.Int if path == "" || path == "/" { return s.prefix, nil } path = paths.Clean(path) // Trim the prefix if it starts with a "/" path = strings.TrimPrefix(path, "/") parts := strings.Split(path, "/") encPath := s.prefix context := strings.TrimSuffix(s.prefix, "/") for _, p := range parts { encoded := base64.StdEncoding.EncodeToString([]byte(p)) ciphertext, err := s.policy.Encrypt(0, []byte(context), nil, encoded) if err != nil { return "", err } encoder.SetBytes([]byte(ciphertext)) encPath = paths.Join(encPath, encoder.Text(62)) context = paths.Join(context, p) } return encPath, nil }