go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/appengine/gaesecrets/gaesecrets.go (about) 1 // Copyright 2015 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package gaesecrets implements storage of secret blobs on top of datastore. 16 // 17 // It is not super secure, but we have what we have: there's no other better 18 // mechanism to persistently store non-static secrets on GAE. 19 // 20 // All secrets are global (live in default GAE namespace). 21 // 22 // TODO(vadimsh): Merge into go.chromium.org/luci/server/gaeemulation once 23 // there are no other users. 24 // 25 // Deprecated: use go.chromium.org/luci/server/secrets instead to fetch secrets 26 // from Google Secret Manager. 27 package gaesecrets 28 29 import ( 30 "context" 31 "crypto/rand" 32 "errors" 33 "io" 34 "strings" 35 "time" 36 37 ds "go.chromium.org/luci/gae/service/datastore" 38 "go.chromium.org/luci/gae/service/info" 39 40 "go.chromium.org/luci/common/clock" 41 "go.chromium.org/luci/common/retry/transient" 42 "go.chromium.org/luci/server/caching" 43 "go.chromium.org/luci/server/secrets" 44 ) 45 46 // TODO(vadimsh): Add secrets rotation. 47 48 // cacheExp is how long to cache secrets in the process memory. 49 const cacheExp = time.Minute * 5 50 51 // Config can be used to tweak parameters of the store. It is fine to use 52 // default values. 53 type Config struct { 54 SecretLen int // length of generated secrets, 32 bytes default 55 Prefix string // optional prefix for entity keys to namespace them 56 Entropy io.Reader // source of random numbers, crypto rand by default 57 } 58 59 // New constructs a secrets.Store implementation that uses datastore. 60 func New(cfg *Config) secrets.Store { 61 config := Config{} 62 if cfg != nil { 63 config = *cfg 64 } 65 if strings.Contains(config.Prefix, ":") { 66 panic("forbidden character ':' in Prefix") 67 } 68 if config.SecretLen == 0 { 69 config.SecretLen = 32 70 } 71 if config.Entropy == nil { 72 config.Entropy = rand.Reader 73 } 74 return &storeImpl{config} 75 } 76 77 // Use injects the GAE implementation of secrets.Store into the context. 78 // The context must be configured with GAE datastore implementation already. 79 func Use(ctx context.Context, cfg *Config) context.Context { 80 return secrets.Use(ctx, New(cfg)) 81 } 82 83 // full secret key (including prefix) => secrets.Secret. 84 var secretsCache = caching.RegisterLRUCache[string, secrets.Secret](100) 85 86 // storeImpl is implementation of secrets.Store bound to a GAE context. 87 type storeImpl struct { 88 cfg Config 89 } 90 91 // RandomSecret returns a secret by its name, generating it if necessary. 92 func (s *storeImpl) RandomSecret(ctx context.Context, k string) (secrets.Secret, error) { 93 return s.getSecret(ctx, k, true) 94 } 95 96 // StoredSecret returns a secret by its name, fetching it from the storage. 97 func (s *storeImpl) StoredSecret(ctx context.Context, k string) (secrets.Secret, error) { 98 return s.getSecret(ctx, k, false) 99 } 100 101 // AddRotationHandler is not implemented. 102 func (s *storeImpl) AddRotationHandler(ctx context.Context, name string, cb secrets.RotationHandler) error { 103 return errors.New("not implemented") 104 } 105 106 func (s *storeImpl) getSecret(ctx context.Context, k string, autogen bool) (secrets.Secret, error) { 107 return secretsCache.LRU(ctx).GetOrCreate(ctx, s.cfg.Prefix+":"+string(k), func() (secrets.Secret, time.Duration, error) { 108 secret, err := s.getSecretFromDatastore(ctx, k, autogen) 109 if err != nil { 110 return secrets.Secret{}, 0, err 111 } 112 return secret, cacheExp, nil 113 }) 114 } 115 116 func (s *storeImpl) getSecretFromDatastore(ctx context.Context, k string, autogen bool) (secrets.Secret, error) { 117 // Switch to default namespace. 118 ctx, err := info.Namespace(ctx, "") 119 if err != nil { 120 panic(err) // should not happen, Namespace errors only on bad namespace name 121 } 122 ctx = ds.WithoutTransaction(ctx) 123 124 // Grab existing. 125 ent := secretEntity{ID: s.cfg.Prefix + ":" + string(k)} 126 err = ds.Get(ctx, &ent) 127 if err != nil && err != ds.ErrNoSuchEntity { 128 return secrets.Secret{}, transient.Tag.Apply(err) 129 } 130 131 // Autogenerate and put into the datastore. 132 if err == ds.ErrNoSuchEntity { 133 if !autogen { 134 return secrets.Secret{}, secrets.ErrNoSuchSecret 135 } 136 ent.Created = clock.Now(ctx).UTC() 137 if ent.Secret, err = s.generateSecret(); err != nil { 138 return secrets.Secret{}, transient.Tag.Apply(err) 139 } 140 err = ds.RunInTransaction(ctx, func(ctx context.Context) error { 141 newOne := secretEntity{ID: ent.ID} 142 switch err := ds.Get(ctx, &newOne); err { 143 case nil: 144 ent = newOne 145 return nil 146 case ds.ErrNoSuchEntity: 147 return ds.Put(ctx, &ent) 148 default: 149 return err 150 } 151 }, nil) 152 if err != nil { 153 return secrets.Secret{}, transient.Tag.Apply(err) 154 } 155 } 156 157 return secrets.Secret{ 158 Active: ent.Secret, 159 }, nil 160 } 161 162 func (s *storeImpl) generateSecret() ([]byte, error) { 163 out := make([]byte, s.cfg.SecretLen) 164 _, err := io.ReadFull(s.cfg.Entropy, out) 165 return out, err 166 } 167 168 //// 169 170 type secretEntity struct { 171 _kind string `gae:"$kind,gaesecrets.Secret"` 172 _extra ds.PropertyMap `gae:"-,extra"` 173 174 ID string `gae:"$id"` 175 176 Secret []byte `gae:",noindex"` // blob with the secret 177 Created time.Time 178 }