go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/secrets/secrets.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 secrets
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"errors"
    21  )
    22  
    23  var (
    24  	// ErrNoSuchSecret indicates the store can't find the requested secret.
    25  	ErrNoSuchSecret = errors.New("secret not found")
    26  	// ErrNoStoreConfigured indicates there's no Store in the context.
    27  	ErrNoStoreConfigured = errors.New("secrets.Store is not in the context")
    28  )
    29  
    30  var contextKey = "secrets.Store"
    31  
    32  // Use installs a Store implementation into the context.
    33  func Use(ctx context.Context, s Store) context.Context {
    34  	return context.WithValue(ctx, &contextKey, s)
    35  }
    36  
    37  // CurrentStore returns a store installed in the context or nil.
    38  func CurrentStore(ctx context.Context) Store {
    39  	store, _ := ctx.Value(&contextKey).(Store)
    40  	return store
    41  }
    42  
    43  // RandomSecret returns a random secret using Store in the context.
    44  //
    45  // If the context doesn't have Store set, returns ErrNoStoreConfigured.
    46  func RandomSecret(ctx context.Context, name string) (Secret, error) {
    47  	if store := CurrentStore(ctx); store != nil {
    48  		return store.RandomSecret(ctx, name)
    49  	}
    50  	return Secret{}, ErrNoStoreConfigured
    51  }
    52  
    53  // StoredSecret returns a stored secret using Store in the context.
    54  //
    55  // If the context doesn't have Store set, returns ErrNoStoreConfigured.
    56  func StoredSecret(ctx context.Context, name string) (Secret, error) {
    57  	if store := CurrentStore(ctx); store != nil {
    58  		return store.StoredSecret(ctx, name)
    59  	}
    60  	return Secret{}, ErrNoStoreConfigured
    61  }
    62  
    63  // AddRotationHandler registers a callback called when the secret is updated.
    64  //
    65  // If the context doesn't have Store set, returns ErrNoStoreConfigured.
    66  func AddRotationHandler(ctx context.Context, name string, cb RotationHandler) error {
    67  	if store := CurrentStore(ctx); store != nil {
    68  		return store.AddRotationHandler(ctx, name, cb)
    69  	}
    70  	return ErrNoStoreConfigured
    71  }
    72  
    73  // Store knows how to retrieve or autogenerate a secret given its name.
    74  //
    75  // See SecretManagerStore for a concrete implementation usually used in
    76  // production.
    77  type Store interface {
    78  	// RandomSecret returns a random secret given its name.
    79  	//
    80  	// The store will auto-generate the secret if necessary. Its value is
    81  	// a random high-entropy blob.
    82  	RandomSecret(ctx context.Context, name string) (Secret, error)
    83  
    84  	// StoredSecret returns a previously stored secret given its name.
    85  	//
    86  	// How it was stored depends on the concrete implementation of the Store. The
    87  	// difference from RandomSecret is that the Store will never try to
    88  	// auto-generate such secret if it is missing and will return ErrNoSuchSecret
    89  	// instead.
    90  	StoredSecret(ctx context.Context, name string) (Secret, error)
    91  
    92  	// AddRotationHandler registers a callback called when the secret is updated.
    93  	//
    94  	// Useful when a value of StoredSecret(...) is used to derive something else.
    95  	// The callback allows the store to notify the consumer of the secret when
    96  	// it changes.
    97  	AddRotationHandler(ctx context.Context, name string, cb RotationHandler) error
    98  }
    99  
   100  // RotationHandler is called from an internal goroutine after the store fetches
   101  // a new version of a stored secret.
   102  type RotationHandler func(context.Context, Secret)
   103  
   104  // Secret represents multiple versions of some secret blob.
   105  //
   106  // There's a current version (which is always set) that should be used for all
   107  // kinds of operations: active (like encryption, signing, etc) and passive
   108  // (like decryption, checking signatures, etc).
   109  //
   110  // And there's zero or more other versions that should be used only for passive
   111  // operations. Other versions contain previous or future values of the secret.
   112  // They are important for implementing graceful rotation of the secret.
   113  type Secret struct {
   114  	Active  []byte   // current value of the secret, always set
   115  	Passive [][]byte // optional list of other values, in no particular order
   116  }
   117  
   118  // Blobs returns the active version and all passive versions as one array.
   119  func (s Secret) Blobs() [][]byte {
   120  	out := make([][]byte, 0, 1+len(s.Passive))
   121  	out = append(out, s.Active)
   122  	out = append(out, s.Passive...)
   123  	return out
   124  }
   125  
   126  // Equal returns true if secrets are equal.
   127  //
   128  // Does *not* run in constant time. Shouldn't be used in a cryptographic
   129  // context due to susceptibility to timing attacks.
   130  func (s Secret) Equal(a Secret) bool {
   131  	switch {
   132  	case len(s.Passive) != len(a.Passive):
   133  		return false
   134  	case !bytes.Equal(s.Active, a.Active):
   135  		return false
   136  	}
   137  	for i, blob := range s.Passive {
   138  		if !bytes.Equal(blob, a.Passive[i]) {
   139  			return false
   140  		}
   141  	}
   142  	return true
   143  }