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 }