github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/core/secrets/createsecret.go (about)

     1  // Copyright 2021 Canonical Ltd.
     2  // Licensed under the LGPLv3, see LICENCE file for details.
     3  
     4  package secrets
     5  
     6  import (
     7  	"encoding/base64"
     8  	"encoding/json"
     9  	"fmt"
    10  	"os"
    11  	"regexp"
    12  	"strings"
    13  
    14  	"github.com/juju/errors"
    15  	"github.com/juju/utils/v3"
    16  	"gopkg.in/yaml.v2"
    17  )
    18  
    19  var keyRegExp = regexp.MustCompile("^([a-z](?:-?[a-z0-9]){2,})$")
    20  
    21  // SecretData holds secret key values.
    22  type SecretData map[string]string
    23  
    24  const (
    25  	fileSuffix          = "#file"
    26  	maxValueSizeBytes   = 8 * 1024
    27  	maxContentSizeBytes = 64 * 1024
    28  )
    29  
    30  // CreateSecretData creates a secret data bag from a list of arguments.
    31  // If a key has the #base64 suffix, then the value is already base64 encoded,
    32  // otherwise the value is base64 encoded as it is added to the data bag.
    33  func CreateSecretData(args []string) (SecretData, error) {
    34  	data := make(SecretData)
    35  	for _, val := range args {
    36  		// Remove any base64 padding ("=") before splitting the key=value.
    37  		stripped := strings.TrimRight(val, string(base64.StdPadding))
    38  		idx := strings.Index(stripped, "=")
    39  		if idx < 1 {
    40  			return nil, errors.NotValidf("key value %q", val)
    41  		}
    42  		keyVal := []string{
    43  			val[0:idx],
    44  			val[idx+1:],
    45  		}
    46  		key := keyVal[0]
    47  		value := keyVal[1]
    48  		if !strings.HasSuffix(key, fileSuffix) {
    49  			data[key] = value
    50  			continue
    51  		}
    52  		key = strings.TrimSuffix(key, fileSuffix)
    53  		path, err := utils.NormalizePath(value)
    54  		if err != nil {
    55  			return nil, errors.Trace(err)
    56  		}
    57  		fs, err := os.Stat(path)
    58  		if err == nil && fs.Size() > maxValueSizeBytes {
    59  			return nil, errors.Errorf("secret content in file %q too large: %d bytes", path, fs.Size())
    60  		}
    61  		content, err := os.ReadFile(value)
    62  		if err != nil {
    63  			return nil, errors.Annotatef(err, "reading content for secret key %q", key)
    64  		}
    65  		data[key] = string(content)
    66  	}
    67  	return encodeBase64(data)
    68  }
    69  
    70  // ReadSecretData reads secret data from a YAML or JSON file as key value pairs.
    71  func ReadSecretData(f string) (SecretData, error) {
    72  	attrs := make(SecretData)
    73  	path, err := utils.NormalizePath(f)
    74  	if err != nil {
    75  		return nil, errors.Trace(err)
    76  	}
    77  	fs, err := os.Stat(path)
    78  	if err == nil && fs.Size() > maxContentSizeBytes {
    79  		return nil, errors.Errorf("secret content in file %q too large: %d bytes", path, fs.Size())
    80  	}
    81  	data, err := os.ReadFile(path)
    82  	if err != nil {
    83  		return nil, errors.Trace(err)
    84  	}
    85  	if err := json.Unmarshal(data, &attrs); err != nil {
    86  		err = yaml.Unmarshal(data, &attrs)
    87  		if err != nil {
    88  			return nil, errors.Trace(err)
    89  		}
    90  	}
    91  	return encodeBase64(attrs)
    92  }
    93  
    94  const base64Suffix = "#base64"
    95  
    96  func encodeBase64(in SecretData) (SecretData, error) {
    97  	out := make(SecretData, len(in))
    98  	var contentSize int
    99  	for k, v := range in {
   100  		if len(v) > maxValueSizeBytes {
   101  			return nil, errors.Errorf("secret content for key %q too large: %d bytes", k, len(v))
   102  		}
   103  		contentSize += len(v)
   104  		if strings.HasSuffix(k, base64Suffix) {
   105  			k = strings.TrimSuffix(k, base64Suffix)
   106  			if !keyRegExp.MatchString(k) {
   107  				return nil, errors.NotValidf("key %q", k)
   108  			}
   109  			out[k] = v
   110  			continue
   111  		}
   112  		if !keyRegExp.MatchString(k) {
   113  			return nil, errors.NotValidf("key %q", k)
   114  		}
   115  		out[k] = base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%v", v)))
   116  	}
   117  	if contentSize > maxContentSizeBytes {
   118  		return nil, errors.Errorf("secret content too large: %d bytes", contentSize)
   119  	}
   120  	return out, nil
   121  }