package keysutil import ( "context" "encoding/base64" "errors" paths "path" "strings" big "github.com/hashicorp/golang-math-big/big" "github.com/hashicorp/golang-lru" "github.com/hashicorp/vault/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 { // Storage is the underlying storage to wrap requests to. Storage logical.Storage // 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 } // NewEncryptedKeyStorage takes an EncryptedKeyStorageConfig and returns a new // EncryptedKeyStorage object. func NewEncryptedKeyStorage(config EncryptedKeyStorageConfig) (*EncryptedKeyStorage, error) { if config.Policy == nil { return nil, ErrNilPolicy } if !config.Policy.Derived { return nil, ErrPolicyDerivedKeys } if !config.Policy.ConvergentEncryption { return nil, ErrPolicyConvergentEncryption } if config.Policy.ConvergentVersion < 2 { return nil, ErrPolicyConvergentVersion } if config.Storage == nil { return nil, ErrNilStorage } 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 &EncryptedKeyStorage{ policy: config.Policy, s: config.Storage, prefix: config.Prefix, lru: cache, }, nil } // 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 } // 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) { encPrefix, err := s.encryptPath(prefix) if err != nil { return nil, err } keys, err := s.s.List(ctx, 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, "/") } decoded := Base62Decode(k) 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 } 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) { path = paths.Clean(path) // Trim the prefix if it starts with a "/" path = strings.TrimPrefix(path, "/") parts := strings.Split(path, "/") encPath := s.prefix context := 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 } encPath = paths.Join(encPath, Base62Encode([]byte(ciphertext))) context = paths.Join(context, p) } return encPath, nil } func Base62Encode(buf []byte) string { encoder := &big.Int{} encoder.SetBytes(buf) return encoder.Text(62) } func Base62Decode(input string) []byte { decoder := &big.Int{} decoder.SetString(input, 62) return decoder.Bytes() }