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  }