go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/secrets/derived.go (about)

     1  // Copyright 2019 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  	"context"
    19  	"crypto/sha256"
    20  	"errors"
    21  	"io"
    22  	"sync"
    23  
    24  	"golang.org/x/crypto/hkdf"
    25  )
    26  
    27  // DerivedStore implements Store by deriving secrets from some single root
    28  // secret using HKDF.
    29  //
    30  // Caches all derived secrets internally forever. Assumes the set of possible
    31  // key names is limited.
    32  type DerivedStore struct {
    33  	m     sync.RWMutex
    34  	root  Secret
    35  	cache map[string]Secret
    36  }
    37  
    38  // NewDerivedStore returns a store that derives secrets from the given root key.
    39  func NewDerivedStore(root Secret) *DerivedStore {
    40  	return &DerivedStore{
    41  		root:  root,
    42  		cache: map[string]Secret{},
    43  	}
    44  }
    45  
    46  // RandomSecret returns a generated secret given its name.
    47  func (d *DerivedStore) RandomSecret(ctx context.Context, name string) (Secret, error) {
    48  	d.m.RLock()
    49  	s, ok := d.cache[name]
    50  	d.m.RUnlock()
    51  	if ok {
    52  		return s, nil
    53  	}
    54  
    55  	d.m.Lock()
    56  	if s, ok = d.cache[name]; !ok {
    57  		s = d.generateLocked(name)
    58  		d.cache[name] = s
    59  	}
    60  	d.m.Unlock()
    61  
    62  	return s, nil
    63  }
    64  
    65  // StoredSecret returns an error, since DerivedStore always derives secrets.
    66  func (d *DerivedStore) StoredSecret(ctx context.Context, name string) (Secret, error) {
    67  	return Secret{}, errors.New("DerivedStore: stored secrets are not supported")
    68  }
    69  
    70  // AddRotationHandler is not implemented.
    71  func (d *DerivedStore) AddRotationHandler(ctx context.Context, name string, cb RotationHandler) error {
    72  	return errors.New("not implemented")
    73  }
    74  
    75  // SetRoot replaces the root key used to derive secrets.
    76  func (d *DerivedStore) SetRoot(root Secret) {
    77  	d.m.RLock()
    78  	same := d.root.Equal(root)
    79  	d.m.RUnlock()
    80  	if !same {
    81  		d.m.Lock()
    82  		d.root = root
    83  		d.cache = map[string]Secret{}
    84  		d.m.Unlock()
    85  	}
    86  }
    87  
    88  func (d *DerivedStore) generateLocked(name string) Secret {
    89  	s := Secret{Active: derive(d.root.Active, name)}
    90  	if len(d.root.Passive) != 0 {
    91  		s.Passive = make([][]byte, len(d.root.Passive))
    92  		for i, secret := range d.root.Passive {
    93  			s.Passive[i] = derive(secret, name)
    94  		}
    95  	}
    96  	return s
    97  }
    98  
    99  func derive(secret []byte, name string) []byte {
   100  	// Note: we don't use salt (nil) because we want the derivation process to be
   101  	// deterministic.
   102  	hkdf := hkdf.New(sha256.New, secret, nil, []byte(name))
   103  	key := make([]byte, 16)
   104  	if _, err := io.ReadFull(hkdf, key); err != nil {
   105  		panic(err)
   106  	}
   107  	return key
   108  }