1package vault 2 3import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "strings" 8 9 "github.com/hashicorp/vault/sdk/framework" 10 "github.com/hashicorp/vault/sdk/helper/jsonutil" 11 "github.com/hashicorp/vault/sdk/logical" 12) 13 14// CubbyholeBackendFactory constructs a new cubbyhole backend 15func CubbyholeBackendFactory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) { 16 b := &CubbyholeBackend{} 17 b.Backend = &framework.Backend{ 18 Help: strings.TrimSpace(cubbyholeHelp), 19 } 20 21 b.Backend.Paths = append(b.Backend.Paths, b.paths()...) 22 23 if conf == nil { 24 return nil, fmt.Errorf("configuration passed into backend is nil") 25 } 26 b.Backend.Setup(ctx, conf) 27 28 return b, nil 29} 30 31// CubbyholeBackend is used for storing secrets directly into the physical 32// backend. The secrets are encrypted in the durable storage. 33// This differs from kv in that every token has its own private 34// storage view. The view is removed when the token expires. 35type CubbyholeBackend struct { 36 *framework.Backend 37 38 saltUUID string 39 storageView logical.Storage 40} 41 42func (b *CubbyholeBackend) paths() []*framework.Path { 43 return []*framework.Path{ 44 { 45 Pattern: framework.MatchAllRegex("path"), 46 47 Fields: map[string]*framework.FieldSchema{ 48 "path": { 49 Type: framework.TypeString, 50 Description: "Specifies the path of the secret.", 51 }, 52 }, 53 54 Operations: map[logical.Operation]framework.OperationHandler{ 55 logical.ReadOperation: &framework.PathOperation{ 56 Callback: b.handleRead, 57 Summary: "Retrieve the secret at the specified location.", 58 }, 59 logical.UpdateOperation: &framework.PathOperation{ 60 Callback: b.handleWrite, 61 Summary: "Store a secret at the specified location.", 62 }, 63 logical.CreateOperation: &framework.PathOperation{ 64 Callback: b.handleWrite, 65 }, 66 logical.DeleteOperation: &framework.PathOperation{ 67 Callback: b.handleDelete, 68 Summary: "Deletes the secret at the specified location.", 69 }, 70 logical.ListOperation: &framework.PathOperation{ 71 Callback: b.handleList, 72 Summary: "List secret entries at the specified location.", 73 Description: "Folders are suffixed with /. The input must be a folder; list on a file will not return a value. The values themselves are not accessible via this command.", 74 }, 75 }, 76 77 ExistenceCheck: b.handleExistenceCheck, 78 79 HelpSynopsis: strings.TrimSpace(cubbyholeHelpSynopsis), 80 HelpDescription: strings.TrimSpace(cubbyholeHelpDescription), 81 }, 82 } 83} 84 85func (b *CubbyholeBackend) revoke(ctx context.Context, view *BarrierView, saltedToken string) error { 86 if saltedToken == "" { 87 return fmt.Errorf("client token empty during revocation") 88 } 89 90 if err := logical.ClearView(ctx, view.SubView(saltedToken+"/")); err != nil { 91 return err 92 } 93 94 return nil 95} 96 97func (b *CubbyholeBackend) handleExistenceCheck(ctx context.Context, req *logical.Request, data *framework.FieldData) (bool, error) { 98 out, err := req.Storage.Get(ctx, req.ClientToken+"/"+req.Path) 99 if err != nil { 100 return false, fmt.Errorf("existence check failed: %w", err) 101 } 102 103 return out != nil, nil 104} 105 106func (b *CubbyholeBackend) handleRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 107 if req.ClientToken == "" { 108 return nil, fmt.Errorf("client token empty") 109 } 110 111 path := data.Get("path").(string) 112 113 if path == "" { 114 return nil, fmt.Errorf("missing path") 115 } 116 117 // Read the path 118 out, err := req.Storage.Get(ctx, req.ClientToken+"/"+path) 119 if err != nil { 120 return nil, fmt.Errorf("read failed: %w", err) 121 } 122 123 // Fast-path the no data case 124 if out == nil { 125 return nil, nil 126 } 127 128 // Decode the data 129 var rawData map[string]interface{} 130 if err := jsonutil.DecodeJSON(out.Value, &rawData); err != nil { 131 return nil, fmt.Errorf("json decoding failed: %w", err) 132 } 133 134 // Generate the response 135 resp := &logical.Response{ 136 Data: rawData, 137 } 138 139 return resp, nil 140} 141 142func (b *CubbyholeBackend) handleWrite(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 143 if req.ClientToken == "" { 144 return nil, fmt.Errorf("client token empty") 145 } 146 // Check that some fields are given 147 if len(req.Data) == 0 { 148 return nil, fmt.Errorf("missing data fields") 149 } 150 151 path := data.Get("path").(string) 152 153 if path == "" { 154 return nil, fmt.Errorf("missing path") 155 } 156 157 // JSON encode the data 158 buf, err := json.Marshal(req.Data) 159 if err != nil { 160 return nil, fmt.Errorf("json encoding failed: %w", err) 161 } 162 163 // Write out a new key 164 entry := &logical.StorageEntry{ 165 Key: req.ClientToken + "/" + path, 166 Value: buf, 167 } 168 if req.WrapInfo != nil && req.WrapInfo.SealWrap { 169 entry.SealWrap = true 170 } 171 if err := req.Storage.Put(ctx, entry); err != nil { 172 return nil, fmt.Errorf("failed to write: %w", err) 173 } 174 175 return nil, nil 176} 177 178func (b *CubbyholeBackend) handleDelete(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 179 if req.ClientToken == "" { 180 return nil, fmt.Errorf("client token empty") 181 } 182 183 path := data.Get("path").(string) 184 185 // Delete the key at the request path 186 if err := req.Storage.Delete(ctx, req.ClientToken+"/"+path); err != nil { 187 return nil, err 188 } 189 190 return nil, nil 191} 192 193func (b *CubbyholeBackend) handleList(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 194 if req.ClientToken == "" { 195 return nil, fmt.Errorf("client token empty") 196 } 197 198 // Right now we only handle directories, so ensure it ends with / We also 199 // check if it's empty so we don't end up doing a listing on '<client 200 // token>//' 201 path := data.Get("path").(string) 202 if path != "" && !strings.HasSuffix(path, "/") { 203 path = path + "/" 204 } 205 206 // List the keys at the prefix given by the request 207 keys, err := req.Storage.List(ctx, req.ClientToken+"/"+path) 208 if err != nil { 209 return nil, err 210 } 211 212 // Strip the token 213 strippedKeys := make([]string, len(keys)) 214 for i, key := range keys { 215 strippedKeys[i] = strings.TrimPrefix(key, req.ClientToken+"/") 216 } 217 218 // Generate the response 219 return logical.ListResponse(strippedKeys), nil 220} 221 222const cubbyholeHelp = ` 223The cubbyhole backend reads and writes arbitrary secrets to the backend. 224The secrets are encrypted/decrypted by Vault: they are never stored 225unencrypted in the backend and the backend never has an opportunity to 226see the unencrypted value. 227 228This backend differs from the 'kv' backend in that it is namespaced 229per-token. Tokens can only read and write their own values, with no 230sharing possible (per-token cubbyholes). This can be useful for implementing 231certain authentication workflows, as well as "scratch" areas for individual 232clients. When the token is revoked, the entire set of stored values for that 233token is also removed. 234` 235 236const cubbyholeHelpSynopsis = ` 237Pass-through secret storage to a token-specific cubbyhole in the storage 238backend, allowing you to read/write arbitrary data into secret storage. 239` 240 241const cubbyholeHelpDescription = ` 242The cubbyhole backend reads and writes arbitrary data into secret storage, 243encrypting it along the way. 244 245The view into the cubbyhole storage space is different for each token; it is 246a per-token cubbyhole. When the token is revoked all values are removed. 247` 248