| // 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 |
| } |