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 }