sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/secretutil/censor.go (about)

     1  /*
     2  Copyright 2021 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // Package secretutil implements utilities to operate on secret data.
    18  package secretutil
    19  
    20  import (
    21  	"encoding/base64"
    22  	"strings"
    23  	"sync"
    24  
    25  	"go4.org/bytereplacer"
    26  )
    27  
    28  // Censorer knows how to replace sensitive data from input.
    29  type Censorer interface {
    30  	// Censor will remove sensitive data previously registered with the Censorer
    31  	// from the input. This is thread-safe, will mutate the input and will never
    32  	// change the overall size of the input.
    33  	Censor(input *[]byte)
    34  }
    35  
    36  func NewCensorer() *ReloadingCensorer {
    37  	return &ReloadingCensorer{
    38  		RWMutex:  &sync.RWMutex{},
    39  		Replacer: bytereplacer.New(),
    40  	}
    41  }
    42  
    43  type ReloadingCensorer struct {
    44  	*sync.RWMutex
    45  	*bytereplacer.Replacer
    46  	largestSecret int
    47  }
    48  
    49  var _ Censorer = &ReloadingCensorer{}
    50  
    51  // Censor will remove sensitive data previously registered with the Censorer
    52  // from the input. This is thread-safe, will mutate the input and will never
    53  // change the overall size of the input.
    54  // Censoring will attempt to be intelligent about how content is removed from
    55  // the input - when the ReloadingCensorer is given secrets to censor, we:
    56  //   - handle the case where whitespace is needed to be trimmed
    57  //   - censor not only the plaintext representation of the secret but also
    58  //     the base64-encoded representation of it, as it's common for k8s
    59  //     Secrets to contain information in this way
    60  func (c *ReloadingCensorer) Censor(input *[]byte) {
    61  	c.RLock()
    62  	// we know our replacer will never have to allocate, as our replacements
    63  	// are the same size as what they're replacing, so we can throw away
    64  	// the return value from Replace()
    65  	c.Replacer.Replace(*input)
    66  	c.RUnlock()
    67  }
    68  
    69  // LargestSecret returns the size of the largest secret we will censor.
    70  func (c *ReloadingCensorer) LargestSecret() int {
    71  	c.RLock()
    72  	defer c.RUnlock()
    73  	return c.largestSecret
    74  }
    75  
    76  // RefreshBytes refreshes the set of secrets that we censor.
    77  func (c *ReloadingCensorer) RefreshBytes(secrets ...[]byte) {
    78  	var asStrings []string
    79  	for _, secret := range secrets {
    80  		asStrings = append(asStrings, string(secret))
    81  	}
    82  	c.Refresh(asStrings...)
    83  }
    84  
    85  // Refresh refreshes the set of secrets that we censor.
    86  func (c *ReloadingCensorer) Refresh(secrets ...string) {
    87  	var largestSecret int
    88  	var replacements []string
    89  	addReplacement := func(s string) {
    90  		replacements = append(replacements, s, strings.Repeat(`X`, len(s)))
    91  		if len(s) > largestSecret {
    92  			largestSecret = len(s)
    93  		}
    94  	}
    95  	for _, secret := range secrets {
    96  		toEncode := []string{secret}
    97  		if trimmed := strings.TrimSpace(secret); trimmed != secret {
    98  			secret = trimmed
    99  			toEncode = append(toEncode, trimmed)
   100  		}
   101  		if secret == "" {
   102  			continue
   103  		}
   104  		if strings.EqualFold(secret, "true") || strings.EqualFold(secret, "false") {
   105  			// true and false appear in secrets when there is configuration that gets changed at the same time actual secrets.
   106  			// this happens when storing credentials to servers where capabilities are different.
   107  			// while every other type could reasonably be secret (through secret floats would be weird), it's difficult
   108  			// to justify secret booleans, since all values are known and replacing them carries a high price.
   109  			continue
   110  		}
   111  
   112  		addReplacement(secret)
   113  		for _, item := range toEncode {
   114  			encoded := base64.StdEncoding.EncodeToString([]byte(item))
   115  			addReplacement(encoded)
   116  		}
   117  	}
   118  	c.Lock()
   119  	c.Replacer = bytereplacer.New(replacements...)
   120  	c.largestSecret = largestSecret
   121  	c.Unlock()
   122  }
   123  
   124  // AdaptCensorer returns a func that censors without touching the input, to
   125  // be used in places where the previous behavior is required while migrations
   126  // occur.
   127  func AdaptCensorer(censorer Censorer) func(input []byte) []byte {
   128  	return func(input []byte) []byte {
   129  		output := make([]byte, len(input))
   130  		copy(output, input)
   131  		censorer.Censor(&output)
   132  		return output
   133  	}
   134  }