├── .gitignore ├── .travis.yml ├── LICENSE ├── api ├── gosecret.go └── gosecret_test.go ├── main.go ├── readme.md ├── template_functions.go ├── template_functions_test.go ├── test_data ├── config.json ├── config_enc.json ├── config_plaintext.json ├── config_special_characters.json ├── config_special_characters_plaintext.json ├── nested │ └── config.xml ├── password └── template │ ├── config.json │ ├── config_hybrid.json │ ├── encrypted.json │ ├── encrypted_hybrid.json │ ├── output.json │ └── output_hybrid.json └── test_keys └── myteamkey-2014-09-19 /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Exclude binary 4 | gosecret 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: go 4 | 5 | go: 6 | - 1.4 7 | 8 | branches: 9 | only: 10 | - master 11 | - /^v\d+.\d+.\d+$/ 12 | 13 | install: 14 | - go get -v github.com/cimpress-mcp/gosecret/... 15 | 16 | script: 17 | - go test github.com/cimpress-mcp/gosecret 18 | - go test github.com/cimpress-mcp/gosecret/api 19 | 20 | after_success: 21 | - test ! $TRAVIS_TAG && exit 22 | - go get -x github.com/mitchellh/gox 23 | - gox -build-toolchain -osarch="linux/amd64 darwin/amd64 windows/amd64" 24 | - gox -output="build/{{.OS}}/{{.Arch}}/{{.Dir}}" -osarch="linux/amd64 darwin/amd64 windows/amd64" 25 | - curl -T build/darwin/amd64/gosecret -uryanbreen:$BINTRAY_KEY https://api.bintray.com/content/cimpress-mcp/Go/gosecret/$TRAVIS_TAG/$TRAVIS_TAG/darwin-amd64/gosecret 26 | - curl -T build/linux/amd64/gosecret -uryanbreen:$BINTRAY_KEY https://api.bintray.com/content/cimpress-mcp/Go/gosecret/$TRAVIS_TAG/$TRAVIS_TAG/linux-amd64/gosecret 27 | - curl -T build/windows/amd64/gosecret.exe -uryanbreen:$BINTRAY_KEY https://api.bintray.com/content/cimpress-mcp/Go/gosecret/$TRAVIS_TAG/$TRAVIS_TAG/windows-amd64/gosecret.exe 28 | - curl -XPOST -uryanbreen:$BINTRAY_KEY https://api.bintray.com/content/cimpress-mcp/Go/gosecret/$TRAVIS_TAG/publish 29 | 30 | env: 31 | global: 32 | - secure: "mHRbR1ckSMWsAxJ90QqrOQZomDPLD6Xj+8EPNFV/jBUSPwqheh2SX7Db0oLwok/OCAYy1lIFA3NrWQ+EodJUaY08f/+HZWQHR4IckNXFMqeDhl1uVX2cv2TRmsy9MJ70CwiM2mfiD2kyEWEsx8H2t4TRIsQ6qil64TNiYrnrqQM=" 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 Cimpress 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 | -------------------------------------------------------------------------------- /api/gosecret.go: -------------------------------------------------------------------------------- 1 | // This repository provides the gosecret package for encrypting and decrypting all or part of a []byte using AES-256-GCM. 2 | // gosecret was written to work with tools such as https://github.com/ryanbreen/git2consul, 3 | // https://github.com/ryanbreen/fsconsul, and https://github.com/hashicorp/envconsul, providing a mechanism for storing 4 | // and moving secure secrets around the network and decrypting them on target systems via a previously installed key. 5 | // 6 | // gosecret is built on the assumption that only part of any given file should be encrypted: in most configuration files, 7 | // there are few fields that need to be encrypted and the rest can safely be left as plaintext. gosecret can be used in a 8 | // mode where the entire file is a single encrypted tag, but you should examine whether there's a good reason to do so. 9 | // 10 | // To signify that you wish a portion of a file to be encrypted, you need to denote that portion of the file with a tag. 11 | // Imagine that your file contains this bit of JSON: 12 | // 13 | // { 'dbpassword': 'kadjf454nkklz' } 14 | // 15 | // To have gosecret encrypt just the password, you might create a tag like this: 16 | // 17 | // { 'dbpassword': '[gosecret|my mongo db password|kadjf454nkklz]' } 18 | // 19 | // The components of the tag are, in order: 20 | // 21 | // 1. The gosecret header 22 | // 2. An auth data string. 23 | // 3. The plaintext we wish to encrypt. 24 | // 25 | // Note that auth data can be any string (as long as it doesn't contain the pipe character, '|'). This tag is hashed and 26 | // included as part of the ciphertext. It's helpful if this tag has some semantic meaning describing the encrypted data. 27 | // Auth data string is not private data. It is hashed and used as part of the ciphertext such that decryption will fail if 28 | // any of auth data, initialization vector, and key are incorrect for a specific piece of ciphertext. This increases the 29 | // security of the encryption algorithm by obviating attacks that seek to learn about the key and initialization vector through 30 | // repeated decryption attempts. 31 | // 32 | // With this tag in place, you can encrypt the file via 'gosecret-cli'. The result will yield something that looks like this, 33 | // assuming you encrypted it with a keyfile named 'myteamkey-2014-09-19': 34 | // 35 | // { 'dbpassword': '[gosecret|my mongo db password|TtRotEctptR1LfA5tSn3kAtzjyWjAp+dMOHe6lc=|FJA7qz+dUdubwv9G|myteamkey-2014-09-19]' } 36 | // 37 | // The components of the tag are, in order: 38 | // 39 | // 1. The gosecret header 40 | // 2. The auth data string 41 | // 3. The ciphertext, in Base64 42 | // 4. The initialization vector, in Base64 43 | // 5. The key name 44 | // 45 | // A key may be used any number of times, but a new initialization vector should be created each time the key is used. This is 46 | // handled for you automatically by gosecret. 47 | // 48 | // When this is decrypted by a system that contains key 'myteamkey-2014-09-19', the key and initialization vector are used to both 49 | // authenticate the auth data string and (if authentic) decrypt the ciphertext back to plaintext. This will result in the 50 | // encrypted tag being replaced by the plaintext, returning us to our original form: 51 | // 52 | // { 'dbpassword': 'kadjf454nkklz' } 53 | // 54 | // A file can contain any number of goscecret tags, or the entire file can be a gosecret tag. It's up to you as the application 55 | // developer or system maintainer to decide what balance of security vs readability you desire. 56 | package api 57 | 58 | import ( 59 | "crypto/aes" 60 | "crypto/cipher" 61 | "crypto/rand" 62 | "encoding/base64" 63 | "errors" 64 | "fmt" 65 | "io/ioutil" 66 | "path/filepath" 67 | "regexp" 68 | "strings" 69 | "unicode/utf8" 70 | ) 71 | 72 | type EncryptionTag struct { 73 | AuthData []byte 74 | Plaintext []byte 75 | KeyName string 76 | } 77 | 78 | type DecryptionTag struct { 79 | AuthData []byte 80 | CipherText []byte 81 | InitVector []byte 82 | KeyName string 83 | } 84 | 85 | //Encrypt the tag, returns the cypher text 86 | func (et *EncryptionTag) EncryptTag(keystore string, iv []byte) ([]byte, error) { 87 | keypath := filepath.Join(keystore, et.KeyName) 88 | key, err := getBytesFromBase64File(keypath) 89 | 90 | aes, err := aes.NewCipher(key) 91 | if err != nil { 92 | return nil, err 93 | } 94 | aesgcm, err := cipher.NewGCM(aes) 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | return aesgcm.Seal(nil, iv, et.Plaintext, et.AuthData), nil 100 | } 101 | 102 | func ParseEncrytionTag(keystore string, s ...string) (DecryptionTag, error) { 103 | // If the function does not contain correct number of arguments 104 | if len(s) != 3 { 105 | return DecryptionTag{}, fmt.Errorf("expected 3 arguments, got %d", len(s)) 106 | } 107 | 108 | //Create EncryptionTag object 109 | et := EncryptionTag{ 110 | []byte(s[0]), 111 | []byte(s[1]), 112 | s[2], 113 | } 114 | 115 | iv := createIV() 116 | cipherText, err := et.EncryptTag(keystore, iv) 117 | if err != nil { 118 | return DecryptionTag{}, err 119 | } 120 | 121 | dt := DecryptionTag { 122 | []byte(s[0]), 123 | cipherText, 124 | iv, 125 | s[2], 126 | } 127 | 128 | return dt, nil 129 | } 130 | 131 | func (dt *DecryptionTag) DecryptTag(keystore string) ([]byte, error) { 132 | 133 | keypath, err := getBytesFromBase64File(filepath.Join(keystore, dt.KeyName)) 134 | if err != nil { 135 | fmt.Println("Unable to read file for decryption", err) 136 | return nil, err 137 | } 138 | 139 | 140 | aesgcm, err := createCipher(keypath) 141 | if err != nil { 142 | return nil, err 143 | } 144 | 145 | return aesgcm.Open(nil, dt.InitVector, dt.CipherText, dt.AuthData) 146 | } 147 | 148 | func ParseDecryptionTag(keystore string, s ...string) (string, error) { 149 | if len(s) != 4 { 150 | return "", fmt.Errorf("expected 4 arguments, go %d", len(s)) 151 | } 152 | 153 | ct, err := base64.StdEncoding.DecodeString(s[1]) 154 | if err != nil { 155 | fmt.Println("Unable to decode ciphertext", err) 156 | return "", err 157 | } 158 | 159 | iv, err := base64.StdEncoding.DecodeString(s[2]) 160 | if err != nil { 161 | fmt.Println("Unable to decode IV", err) 162 | return "", err 163 | } 164 | 165 | dt := DecryptionTag{ 166 | []byte(s[0]), 167 | ct, 168 | iv, 169 | s[3], 170 | } 171 | 172 | plaintext, err := dt.DecryptTag(keystore) 173 | if err != nil { 174 | return "", err 175 | } 176 | 177 | return string(plaintext), nil 178 | } 179 | 180 | ////////////////////////////////////////////// 181 | // Old functions and methods for handling tags 182 | ////////////////////////////////////////////// 183 | 184 | var gosecretRegex, _ = regexp.Compile("\\[(gosecret\\|[^\\]]*)\\]") 185 | 186 | // Create a random array of bytes. This is used to create keys and IVs. 187 | func createRandomBytes(length int) []byte { 188 | random_bytes := make([]byte, length) 189 | rand.Read(random_bytes) 190 | return random_bytes 191 | } 192 | 193 | // Create a random 256-bit array suitable for use as an AES-256 cipher key. 194 | func CreateKey() []byte { 195 | return createRandomBytes(32) 196 | } 197 | 198 | // Create a random initialization vector to use for encryption. Each gosecret tag should have a different 199 | // initialization vector. 200 | func createIV() []byte { 201 | return createRandomBytes(12) 202 | } 203 | 204 | // Create an AES-256 GCM cipher for use by gosecret. This is the only form of encryption supported by gosecret, 205 | // and barring any major flaws being discovered 256-bit keys should be adequate for quite some time. 206 | func createCipher(key []byte) (cipher.AEAD, error) { 207 | aes, err := aes.NewCipher(key) 208 | if err != nil { 209 | return nil, err 210 | } 211 | aesgcm, err := cipher.NewGCM(aes) 212 | if err != nil { 213 | return nil, err 214 | } 215 | return aesgcm, nil 216 | } 217 | 218 | // Given an input plaintext []byte and key, initialization vector, and auth data []bytes, encrypt the plaintext 219 | // using an AES-GCM cipher and return a []byte containing the result. 220 | func encrypt(plaintext, key, iv, ad []byte) ([]byte, error) { 221 | aesgcm, err := createCipher(key) 222 | if err != nil { 223 | return nil, err 224 | } 225 | return aesgcm.Seal(nil, iv, plaintext, ad), nil 226 | } 227 | 228 | // Given an input ciphertext []byte and the key, initialization vector, and auth data []bytes used to encrypt it, 229 | // decrypt using an AES-GCM cipher and return a []byte containing the result. 230 | func decrypt(ciphertext, key, iv, ad []byte) ([]byte, error) { 231 | aesgcm, err := createCipher(key) 232 | if err != nil { 233 | return nil, err 234 | } 235 | 236 | return aesgcm.Open(nil, iv, ciphertext, ad) 237 | } 238 | 239 | // Given an input []byte of Base64 encoded data, return a slice containing the decoded data. 240 | func decodeBase64(input []byte) ([]byte, error) { 241 | output := make([]byte, base64.StdEncoding.DecodedLen(len(input))) 242 | l, err := base64.StdEncoding.Decode(output, input) 243 | 244 | if err != nil { 245 | return nil, err 246 | } 247 | 248 | return output[:l], nil 249 | } 250 | 251 | // Given a file path known to contain Base64 encoded data, return a slice containing the decoded data. 252 | func getBytesFromBase64File(filepath string) ([]byte, error) { 253 | file, err := ioutil.ReadFile(filepath) 254 | if err != nil { 255 | fmt.Println("Unable to read file", err) 256 | return nil, err 257 | } 258 | 259 | return decodeBase64(file) 260 | } 261 | 262 | // Given an array of encrypted tag parts and a directory of keys, convert the encrypted gosecret tag into 263 | // a plaintext []byte. 264 | func decryptTag(tagParts []string, keyroot string) ([]byte, error) { 265 | ct, err := base64.StdEncoding.DecodeString(tagParts[2]) 266 | if err != nil { 267 | fmt.Println("Unable to decode ciphertext", tagParts[2], err) 268 | return nil, err 269 | } 270 | 271 | iv, err := base64.StdEncoding.DecodeString(tagParts[3]) 272 | if err != nil { 273 | fmt.Println("Unable to decode IV", err) 274 | return nil, err 275 | } 276 | 277 | key, err := getBytesFromBase64File(filepath.Join(keyroot, tagParts[4])) 278 | if err != nil { 279 | fmt.Println("Unable to read file for decryption", err) 280 | return nil, err 281 | } 282 | 283 | plaintext, err := decrypt(ct, key, iv, []byte(tagParts[1])) 284 | if err != nil { 285 | return nil, err 286 | } 287 | 288 | return plaintext, nil 289 | } 290 | 291 | // Given an array of unencrypted tag parts, a []byte containing the key, and a name for the key, generate 292 | // an encrypted gosecret tag. 293 | func encryptTag(tagParts []string, key []byte, keyname string) ([]byte, error) { 294 | iv := createIV() 295 | cipherText, err := encrypt([]byte(tagParts[2]), key, iv, []byte(tagParts[1])) 296 | if err != nil { 297 | return []byte(""), err 298 | } 299 | 300 | return []byte(fmt.Sprintf("[gosecret|%s|%s|%s|%s]", 301 | tagParts[1], 302 | base64.StdEncoding.EncodeToString(cipherText), 303 | base64.StdEncoding.EncodeToString(iv), 304 | keyname)), nil 305 | } 306 | 307 | // EncryptTags looks for any tagged data of the form [gosecret|authtext|plaintext] in the input content byte 308 | // array and replaces each with an encrypted gosecret tag. Note that the input content must be valid UTF-8. 309 | // The second parameter is the name of the keyfile to use for encrypting all tags in the content, and the 310 | // third parameter is the 256-bit key itself. 311 | // EncryptTags returns a []byte with all unencrypted [gosecret] blocks replaced by encrypted gosecret tags. 312 | func EncryptTags(content []byte, keyname, keyroot string, rotate bool) ([]byte, error) { 313 | 314 | if !utf8.Valid(content) { 315 | return nil, errors.New("File is not valid UTF-8") 316 | } 317 | 318 | match := gosecretRegex.Match(content) 319 | 320 | if match { 321 | 322 | keypath := filepath.Join(keyroot, keyname) 323 | key, err := getBytesFromBase64File(keypath) 324 | if err != nil { 325 | fmt.Println("Unable to read encryption key") 326 | return nil, err 327 | } 328 | 329 | content = gosecretRegex.ReplaceAllFunc(content, func(match []byte) []byte { 330 | matchString := string(match) 331 | matchString = matchString[:len(matchString)-1] 332 | parts := strings.Split(string(matchString), "|") 333 | 334 | if len(parts) > 3 { 335 | if rotate { 336 | plaintext, err := decryptTag(parts, keyroot) 337 | if err != nil { 338 | fmt.Println("Unable to decrypt ciphertext", parts[2], err) 339 | return nil 340 | } 341 | 342 | parts[2] = string(plaintext) 343 | 344 | replacement, err := encryptTag(parts, key, keyname) 345 | if err != nil { 346 | fmt.Println("Failed to encrypt tag", err) 347 | return nil 348 | } 349 | return replacement 350 | } else { 351 | return match 352 | } 353 | } else { 354 | replacement, err := encryptTag(parts, key, keyname) 355 | if err != nil { 356 | fmt.Println("Failed to encrypt tag", err) 357 | return nil 358 | } 359 | return replacement 360 | } 361 | }) 362 | } 363 | 364 | return content, nil 365 | } 366 | 367 | // DecryptTags looks for any tagged data of the form [gosecret|authtext|ciphertext|initvector|keyname] in the 368 | // input content byte array and replaces each with a decrypted version of the ciphertext. Note that the 369 | // input content must be valid UTF-8. The second parameter is the path to the directory in which keyfiles 370 | // live. For each |keyname| in a gosecret block, there must be a corresponding file of the same name in the 371 | // keystore directory. 372 | // DecryptTags returns a []byte with all [gosecret] blocks replaced by plaintext. 373 | func DecryptTags(content []byte, keyroot string) ([]byte, error) { 374 | 375 | if !utf8.Valid(content) { 376 | return nil, errors.New("File is not valid UTF-8") 377 | } 378 | 379 | content = gosecretRegex.ReplaceAllFunc(content, func(match []byte) []byte { 380 | matchString := string(match) 381 | matchString = matchString[:len(matchString)-1] 382 | parts := strings.Split(matchString, "|") 383 | 384 | if len(parts) < 5 { 385 | // Block is not encrypted. Noop. 386 | return match 387 | } else { 388 | plaintext, err := decryptTag(parts, keyroot) 389 | if err != nil { 390 | fmt.Println("Unable to decrypt tag", err) 391 | return nil 392 | } 393 | 394 | return plaintext 395 | } 396 | }) 397 | 398 | return content, nil 399 | } 400 | -------------------------------------------------------------------------------- /api/gosecret_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "io/ioutil" 7 | "os" 8 | "path" 9 | "testing" 10 | ) 11 | 12 | func TestEncryptTag(t *testing.T) { 13 | 14 | et := EncryptionTag{ 15 | []byte("MySql Password"), 16 | []byte( "kadjf454nkklz"), 17 | "myteamkey-2014-09-19", 18 | } 19 | 20 | keystore := path.Clean("../test_keys") 21 | iv := createIV() 22 | 23 | cipherText, err := et.EncryptTag(keystore, iv) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | dt := DecryptionTag { 29 | []byte("MySql Password"), 30 | cipherText, 31 | iv, 32 | "myteamkey-2014-09-19", 33 | } 34 | 35 | plaintext, err := dt.DecryptTag(keystore) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | 40 | if !bytes.Equal(plaintext, []byte("kadjf454nkklz")) { 41 | t.Error("Decrypt failed") 42 | } 43 | } 44 | 45 | func TestParsingTag(t *testing.T) { 46 | keystore := path.Clean("../test_keys") 47 | 48 | dt, err := ParseEncrytionTag(keystore, "MySql Password", "kadjf454nkklz", "myteamkey-2014-09-19") 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | 53 | plaintext, err := ParseDecryptionTag(keystore, string(dt.AuthData), base64.StdEncoding.EncodeToString(dt.CipherText), base64.StdEncoding.EncodeToString(dt.InitVector), dt.KeyName) 54 | if err != nil { 55 | t.Fatal(err) 56 | } 57 | 58 | if !(plaintext == "kadjf454nkklz") { 59 | t.Error("Decrypt failed") 60 | } 61 | } 62 | 63 | /////////////////// 64 | // End of new tests 65 | /////////////////// 66 | 67 | func Testencrypt(t *testing.T) { 68 | 69 | key := CreateKey() 70 | iv := createIV() 71 | 72 | plaintext := []byte("Secret to encrypt.") 73 | auth_data := []byte("scrt") 74 | 75 | cipher_text, err := encrypt(plaintext, key, iv, auth_data) 76 | if err != nil { 77 | t.Fatal(err) 78 | } 79 | 80 | plaintext2, err := decrypt(cipher_text, key, iv, auth_data) 81 | if err != nil { 82 | t.Fatal(err) 83 | } 84 | 85 | if !bytes.Equal(plaintext, plaintext2) { 86 | t.Error("Decrypt failed") 87 | } 88 | } 89 | 90 | func TestNoopEncryptFile(t *testing.T) { 91 | 92 | var original []byte = []byte("This string is not encrypted. It should be returned without modification.") 93 | 94 | notEncrypted, err := EncryptTags(original, "myteamkey-2014-09-19", "test_keys", false) 95 | if err != nil { 96 | t.Fatal(err) 97 | } 98 | 99 | if !bytes.Equal(original, notEncrypted) { 100 | t.Error("No-op encryption failed") 101 | } 102 | } 103 | 104 | func TestNoopDecryptFile(t *testing.T) { 105 | 106 | var original []byte = []byte("This string is not encrypted. It should be returned without modification.") 107 | 108 | notDecrypted, err := DecryptTags(original, "test_keys") 109 | if err != nil { 110 | t.Fatal(err) 111 | } 112 | 113 | if !bytes.Equal(original, notDecrypted) { 114 | t.Error("No-op decryption failed") 115 | } 116 | } 117 | 118 | func TestEncryptSpecialCharacterFile(t *testing.T) { 119 | 120 | plaintextFile, err := ioutil.ReadFile(path.Join("../test_data", "config_special_characters_plaintext.json")) 121 | if err != nil { 122 | t.Fatal(err) 123 | } 124 | 125 | file, err := ioutil.ReadFile(path.Join("../test_data", "config_special_characters.json")) 126 | if err != nil { 127 | t.Fatal(err) 128 | } 129 | 130 | encrypted, err := EncryptTags(file, "myteamkey-2014-09-19", "../test_keys", false) 131 | if err != nil { 132 | t.Fatal(err) 133 | } 134 | 135 | decrypted, err := DecryptTags(encrypted, "../test_keys") 136 | 137 | if err != nil { 138 | t.Fatal(err) 139 | } 140 | 141 | if !bytes.Equal(plaintextFile, decrypted) { 142 | t.Error("Encrypt / Decrypt round-trip failed") 143 | } 144 | } 145 | 146 | func TestEncryptFile(t *testing.T) { 147 | 148 | plaintextFile, err := ioutil.ReadFile(path.Join("../test_data", "config_plaintext.json")) 149 | if err != nil { 150 | t.Fatal(err) 151 | } 152 | 153 | file, err := ioutil.ReadFile(path.Join("../test_data", "config.json")) 154 | if err != nil { 155 | t.Fatal(err) 156 | } 157 | 158 | encrypted, err := EncryptTags(file, "myteamkey-2014-09-19", "../test_keys", false) 159 | if err != nil { 160 | t.Fatal(err) 161 | } 162 | 163 | decrypted, err := DecryptTags(encrypted, "../test_keys") 164 | 165 | if err != nil { 166 | t.Fatal(err) 167 | } 168 | 169 | if !bytes.Equal(plaintextFile, decrypted) { 170 | t.Error("Encrypt / Decrypt round-trip failed") 171 | } 172 | } 173 | 174 | func TestKeyRotation(t *testing.T) { 175 | 176 | os.Remove(path.Join("test_keys", "test_key_1")) 177 | os.Remove(path.Join("test_keys", "test_key_2")) 178 | 179 | plaintextFile, err := ioutil.ReadFile(path.Join("../test_data", "config_plaintext.json")) 180 | if err != nil { 181 | t.Fatal(err) 182 | } 183 | 184 | file, err := ioutil.ReadFile(path.Join("../test_data", "config.json")) 185 | if err != nil { 186 | t.Fatal(err) 187 | } 188 | 189 | // Create a new key. Use it to encrypt. 190 | rawKey := CreateKey() 191 | key := make([]byte, base64.StdEncoding.EncodedLen(len(rawKey))) 192 | base64.StdEncoding.Encode(key, rawKey) 193 | err = ioutil.WriteFile(path.Join("../test_keys", "test_key_1"), key, 0666) 194 | if err != nil { 195 | t.Fatal(err) 196 | } 197 | 198 | encrypted, err := EncryptTags(file, "test_key_1", "../test_keys", false) 199 | if err != nil { 200 | t.Fatal(err) 201 | } 202 | 203 | // Create another new key. Use it to re-encrypt. 204 | rawKey = CreateKey() 205 | key = make([]byte, base64.StdEncoding.EncodedLen(len(rawKey))) 206 | base64.StdEncoding.Encode(key, rawKey) 207 | err = ioutil.WriteFile(path.Join("../test_keys", "test_key_2"), key, 0666) 208 | if err != nil { 209 | t.Fatal(err) 210 | } 211 | 212 | encrypted, err = EncryptTags(file, "test_key_2", "../test_keys", true) 213 | if err != nil { 214 | t.Fatal(err) 215 | } 216 | 217 | // Delete the first key. 218 | err = os.Remove(path.Join("../test_keys", "test_key_1")) 219 | if err != nil { 220 | t.Fatal(err) 221 | } 222 | 223 | // Decrypt the file 224 | decrypted, err := DecryptTags(encrypted, "../test_keys") 225 | if err != nil { 226 | t.Fatal(err) 227 | } 228 | 229 | os.Remove(path.Join("../test_keys", "test_key_2")) 230 | 231 | if !bytes.Equal(plaintextFile, decrypted) { 232 | t.Error("Encrypt / Decrypt round-trip failed") 233 | } 234 | } 235 | 236 | func TestDecryptFile(t *testing.T) { 237 | 238 | plaintextFile, err := ioutil.ReadFile(path.Join("../test_data", "config_plaintext.json")) 239 | if err != nil { 240 | t.Fatal(err) 241 | } 242 | 243 | file, err := ioutil.ReadFile(path.Join("../test_data", "config_enc.json")) 244 | if err != nil { 245 | t.Fatal(err) 246 | } 247 | 248 | fileContents, err := DecryptTags(file, "../test_keys") 249 | 250 | if err != nil { 251 | t.Fatal(err) 252 | } 253 | 254 | if !bytes.Equal(plaintextFile, fileContents) { 255 | t.Error("Decrypt failed") 256 | } 257 | } 258 | 259 | func BenchmarkEncryptFile(b *testing.B) { 260 | 261 | file, err := ioutil.ReadFile(path.Join("../test_data", "config.json")) 262 | if err != nil { 263 | b.Fatal(err) 264 | } 265 | 266 | b.ResetTimer() 267 | 268 | for i := 0; i < b.N; i++ { 269 | _, err := EncryptTags(file, "myteamkey-2014-09-19", "../test_keys", false) 270 | if err != nil { 271 | b.Fatal(err) 272 | } 273 | } 274 | } 275 | 276 | func BenchmarkDecryptFile(b *testing.B) { 277 | 278 | file, err := ioutil.ReadFile(path.Join("../test_data", "config_enc.json")) 279 | if err != nil { 280 | b.Fatal(err) 281 | } 282 | 283 | b.ResetTimer() 284 | 285 | for i := 0; i < b.N; i++ { 286 | _, err := DecryptTags(file, "../test_keys") 287 | if err != nil { 288 | b.Fatal(err) 289 | } 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "flag" 7 | "fmt" 8 | gosecret "github.com/cimpress-mcp/gosecret/api" 9 | "io/ioutil" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | "text/template" 14 | ) 15 | 16 | func main() { 17 | os.Exit(realMain()) 18 | } 19 | 20 | func realMain() int { 21 | var mode string 22 | var value string 23 | var keystore string 24 | var keyname string 25 | var rotate bool 26 | var fileName string 27 | flag.Usage = usage 28 | flag.StringVar( 29 | &mode, "mode", "encrypt", 30 | "mode of operation, either keygen, encrypt, or decrypt; defaults to encrypt") 31 | flag.StringVar( 32 | &value, "value", "", 33 | "value to encrypt/decrypt in lieu of file") 34 | flag.StringVar( 35 | &keystore, "keystore", "/keys/", 36 | "directory in which keys are stored") 37 | flag.StringVar( 38 | &keyname, "key", "", 39 | "name of a key file to use for encryption") 40 | flag.BoolVar( 41 | &rotate, "rotate", true, 42 | "if encrypting, whether to rotate any already-encrypted tags to the new key") 43 | flag.Parse() 44 | if value == "" { 45 | if flag.NArg() != 1 { 46 | flag.Usage() 47 | return 1 48 | } else { 49 | fileName = flag.Args()[0] 50 | } 51 | } 52 | if mode == "encrypt" { 53 | if (keyname == "") { 54 | fmt.Println("A -key must be provided for encryption") 55 | return 2 56 | } 57 | rawBytes := getBytes(value, fileName) 58 | 59 | fileContents, err := gosecret.EncryptTags(rawBytes, keyname, keystore, rotate) 60 | if (err != nil) { 61 | fmt.Println("encryption failed", err) 62 | return 4 63 | } 64 | 65 | data := string(fileContents) 66 | 67 | // Create a template, add the function map, and parse the text. 68 | // FuncMap maps the goEncrypt (and goDecrypt below) names to functions so that the 69 | // template recognizes and knows what to do when it encounters such tags during parsing 70 | funcs := template.FuncMap{ 71 | // Template functions 72 | "goEncrypt": goEncryptFunc(keystore), 73 | } 74 | 75 | tmpl, err := template.New("encryption").Funcs(funcs).Parse(data) 76 | if err != nil { 77 | fmt.Println("Could not parse template", err) 78 | return 99 79 | } 80 | 81 | // Run the template to verify the output. 82 | buff := new(bytes.Buffer) 83 | err = tmpl.Execute(buff, nil) 84 | if err != nil { 85 | fmt.Println("Could not execute template", err) 86 | return 98 87 | } 88 | 89 | fmt.Printf(string(buff.Bytes())) 90 | 91 | } else if mode == "decrypt" { 92 | rawBytes := getBytes(value, fileName) 93 | fileContents, err := gosecret.DecryptTags(rawBytes, keystore) 94 | if (err != nil) { 95 | fmt.Println("err", err) 96 | return 8 97 | } 98 | 99 | data := string(fileContents) 100 | 101 | funcs := template.FuncMap{ 102 | // Template functions 103 | "goDecrypt": goDecryptFunc(keystore), 104 | } 105 | 106 | tmpl, err := template.New("decryption").Funcs(funcs).Parse(data) 107 | if err != nil { 108 | fmt.Println("Could not parse template", err) 109 | return 99 110 | } 111 | 112 | // Run the template to verify the output. 113 | buff := new(bytes.Buffer) 114 | err = tmpl.Execute(buff, nil) 115 | if err != nil { 116 | fmt.Println("Could not execute template", err) 117 | return 98 118 | } 119 | 120 | fmt.Printf(string(buff.Bytes())) 121 | 122 | } else if mode == "keygen" { 123 | key := gosecret.CreateKey() 124 | encodedKey := make([]byte, base64.StdEncoding.EncodedLen(len(key))) 125 | base64.StdEncoding.Encode(encodedKey, key) 126 | ioutil.WriteFile(fileName, encodedKey, 0666) 127 | } else { 128 | fmt.Println("Unknown mode", mode) 129 | return 16 130 | } 131 | 132 | return 0 133 | } 134 | 135 | func getBytes(value string, fileName string) []byte { 136 | if value != "" { 137 | return []byte(value) 138 | } 139 | file, err := ioutil.ReadFile(fileName) 140 | if err != nil { 141 | fmt.Println("Unable to read file for encryption", err) 142 | return nil 143 | } 144 | return file 145 | } 146 | 147 | func usage() { 148 | cmd := filepath.Base(os.Args[0]) 149 | fmt.Fprintf(os.Stderr, strings.TrimSpace(helpText)+"\n\n", cmd) 150 | flag.PrintDefaults() 151 | } 152 | 153 | const helpText = ` 154 | Usage: %s [options] file 155 | 156 | Encrypt or decrypt file using gosecret. 157 | 158 | Options: 159 | ` 160 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | gosecret 2 | ======== 3 | [![Build Status](https://travis-ci.org/Cimpress-MCP/gosecret.svg?branch=master)][travis] 4 | [![Download](https://api.bintray.com/packages/cimpress-mcp/Go/gosecret/images/download.svg)][bintray] 5 | [![Go Documentation](http://img.shields.io/badge/go-documentation-blue.svg)][godocs] 6 | 7 | [travis]: http://travis-ci.org/Cimpress-MCP/gosecret 8 | [bintray]: https://bintray.com/cimpress-mcp/Go/gosecret/_latestVersion#files 9 | [godocs]: http://godoc.org/github.com/Cimpress-MCP/gosecret/api 10 | 11 | 12 | This repository provides the `gosecret` package for encrypting and decrypting all or part of a `[]byte` using AES-256-GCM. gosecret was written to work with tools such as [git2consul](https://github.com/Cimpress-MCP/git2consul), [fsconsul](https://github.com/Cimpress-MCP/fsconsul), and [envconsul](https://github.com/hashicorp/envconsul), providing a mechanism for storing and moving secure secrets around the network and decrypting them on target systems via a previously installed key. 13 | 14 | Details on the Galois/Counter Mode algorithm can be found [here](https://en.wikipedia.org/wiki/Galois/Counter_Mode). 15 | 16 | Installation 17 | ------------ 18 | 19 | Download versioned binaries from [Bintray](https://bintray.com/cimpress-mcp/Go/gosecret/_latestVersion#files). Here's an example of how to automate the download: 20 | 21 | ```bash 22 | # Download gosecret v0.5.0 from Bintray to the working directory 23 | curl -L "https://bintray.com/artifact/download/cimpress-mcp/Go/v0.5.0/linux-amd64/gosecret" -o bin/gosecret 24 | chmod +x ./bin/gosecret 25 | ``` 26 | 27 | Optionally, gosecret can be built and installed from source by cloning the repository and executing `go install`, which will install both the `gosecret` CLI to `$GOPATH/bin` and `github.com/cimpress-mcp/gosecret/api` to `$GOPATH/pkg`. 28 | 29 | Using the native template system 30 | -------------------------------- 31 | 32 | Gosecret supports using native template tags. The tag format is different from the one previously used by gosecret, which is currently deprecated and will be removed in the next major release. 33 | 34 | Encrypting and decrypting keys require appropriate tags. In the case of encryption, gosecret will turn all `goEncrypt` tags to `goDecrypt` tags. Running gosecret in decryption mode will turn `goDecrypt` tags into plaintext data. Please note that quotes will need to be escaped if they are part of the plaintext. 35 | 36 | #### The goEncrypt tag 37 | 38 | `{{goEncrypt "Auth data" "Plaintext" "Key name"}}` 39 | 40 | To encrypt: 41 | ``` 42 | $ ./gosecret -mode encrypt -keystore ./test_keys -key myteamkey-2014-09-19 ./test_data/template/config.json 43 | { 44 | "dbpassword" : "{{goDecrypt "MySql Password" "LcKxOXJa2qx1Riof0tLKzXvKW93ukxgOOBhspoc=" "fpY9FRvJ+8Z7ko6M" "myteamkey-2014-09-19"}}" 45 | } 46 | ``` 47 | 48 | #### The goDecrypt tag 49 | 50 | `{{goDecrypt "Auth data" "Cipher text" "Initialization vector" "Key name"}}` 51 | 52 | To decrypt: 53 | ``` 54 | $ ./gosecret -mode decrypt -keystore ./test_keys ./test_data/template/encrypted.json 55 | { 56 | "dbpassword" : "kadjf454nkklz" 57 | } 58 | ``` 59 | 60 | #### Key generation 61 | 62 | To generate a AES-256 key: 63 | 64 | ``` 65 | gosecret -mode keygen ./test_keys/myteamkey-2014-09-19 66 | ``` 67 | 68 | Documentation (deprecated) 69 | ------------- 70 | ### Caveats 71 | 72 | * Security in gosecret is predicated upon the security of the target machines. gosecret uses symmetric encryption, so any user with access to the key can decrypt all secrets encrypted with that key. 73 | * gosecret is built on the assumption that only part of any given file should be encrypted: in most configuration files, there are few fields that need to be encrypted and the rest can safely be left as plaintext. gosecret can be used in a mode where the entire file is a single encrypted tag, but you should examine whether there's a good reason to do so. 74 | 75 | ### How It Works 76 | 77 | Imagine that you have a file called `config.json`, and this file contains some secure data, such as DB connection strings, but that most data can be world readable. You would use `gosecret` to encrypt the private fields with a specific key. You can then check that version of `config.json` into git and move it around the network using `git2consul`. `fsconsul` will detect the encrypted portion of the file and automatically decrypt it provided that the encryption key is present on the target machine. 78 | 79 | To signify that you wish a portion of a file to be encrypted, you need to denote that portion of the file with a tag. Imagine that your file contains this bit of JSON: 80 | 81 | { 'dbpassword': 'kadjf454nkklz' } 82 | 83 | To have gosecret encrypt just the password, you might create a tag like this: 84 | 85 | { 'dbpassword': '[gosecret|my mongo db password|kadjf454nkklz]' } 86 | 87 | The components of the tag are, in order: 88 | 89 | 1. The gosecret header 90 | 2. An auth data string. Note that this can be any string (as long as it doesn't contain the pipe character, `|`). This tag is hashed and included as part of the ciphertext. It's helpful if this tag has some semantic meaning describing the encrypted data. 91 | 3. The plaintext we wish to encrypt. 92 | 93 | With this tag in place, you can encrypt the file via the `gosecret` executable. The result will yield something that looks like this, assuming you encrypted it with a keyfile named `myteamkey-2014-09-19`: 94 | 95 | { 'dbpassword': '[gosecret|my mongo db password|TtRotEctptR1LfA5tSn3kAtzjyWjAp+dMOHe6lc=|FJA7qz+dUdubwv9G|myteamkey-2014-09-19]' } 96 | 97 | The components of the tag are, in order: 98 | 99 | 1. The gosecret header 100 | 2. The auth data string 101 | 3. The ciphertext, in Base64 102 | 4. The initialization vector, in Base64 103 | 5. The key name 104 | 105 | When this is decrypted by a system that contains key `myteamkey-2014-09-19`, the key and initialization vector are used to both authenticate the auth data string and (if authentic) decrypt the ciphertext back to plaintext. This will result in the encrypted tag being replaced by the plaintext, returning us to our original form: 106 | 107 | { 'dbpassword': 'kadjf454nkklz' } 108 | 109 | Note that the auth data string is not private data. It is hashed and used as part of the ciphertext such that decryption will fail if any of auth data, initialization vector, and key are incorrect for a specific piece of ciphertext. This increases the security of the encryption algorithm by obviating attacks that seek to learn about the key and initialization vector through repeated decryption attempts. 110 | 111 | ### The CLI 112 | 113 | `gosecret` supports 3 modes of operation: `keygen`, `encrypt`, and `decrypt`. 114 | 115 | #### keygen 116 | 117 | `gosecret -mode=keygen path/to/keyfile` 118 | 119 | The above command will generate a new AES-256 key and store it, Base64 encoded, in `path/to/keyfile` 120 | 121 | #### encrypt 122 | 123 | `gosecret -mode=encrypt -keystore=path/to/keystore -key=name_of_keyfile path/to/plaintext_file` 124 | 125 | The above command will encrypt any unencrypted tags in `path/to/plaintext_file` using the key stored at `path/to/keyfile`. The encrypted file is printed to stdout. 126 | 127 | #### decrypt 128 | 129 | `gosecret -mode=decrypt -keystore=path/to/keystore path/to/encrypted_file` 130 | 131 | The above command will decrypt any encrypted tags in `path/to/encrypted_file`, using the directory `path/to/keystore` as the home for any key named in an encrypted tag. The decrypted file is printed to stdout. 132 | 133 | ## Notes 134 | 135 | **Deprecation notice:** Old templating system is deprecated and will be removed in future versions of gosecret. 136 | 137 | Builds are automatically ran by Travis on any push or pull request. 138 | 139 | Tagged builds are automatically published to bintray for OS X, Linux, and Windows. 140 | 141 | Licensed under Apache 2.0 142 | -------------------------------------------------------------------------------- /template_functions.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | gosecret "github.com/cimpress-mcp/gosecret/api" 7 | ) 8 | 9 | func goEncryptFunc(keystore string) func(...string) (string, error) { 10 | return func(s ...string) (string, error) { 11 | dt, err := gosecret.ParseEncrytionTag(keystore, s...) 12 | if err != nil { 13 | fmt.Println("Unable to parse encryption tag", err) 14 | return "", err 15 | } 16 | 17 | return (fmt.Sprintf("{{goDecrypt \"%s\" \"%s\" \"%s\" \"%s\"}}", 18 | dt.AuthData, 19 | base64.StdEncoding.EncodeToString(dt.CipherText), 20 | base64.StdEncoding.EncodeToString(dt.InitVector), 21 | dt.KeyName)), nil 22 | } 23 | } 24 | 25 | func goDecryptFunc(keystore string) func(...string) (string, error) { 26 | return func(s ...string) (string, error) { 27 | plaintext, err := gosecret.ParseDecryptionTag(keystore, s...) 28 | if err != nil { 29 | fmt.Println("Unable to parse encryption tag", err) 30 | return "", err 31 | } 32 | 33 | return fmt.Sprintf("%s", plaintext), nil 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /template_functions_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "path" 5 | "reflect" 6 | "regexp" 7 | "testing" 8 | ) 9 | 10 | func TestGoEncryptFunc(t *testing.T) { 11 | keystore := path.Clean("./test_keys") 12 | 13 | f := goEncryptFunc(keystore) 14 | 15 | result, err := f("MySql Password", "kadjf454nkklz", "myteamkey-2014-09-19") 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | 20 | //Regexp on decryption tag 21 | matched, err := regexp.MatchString("{{goDecrypt.*}}", result) 22 | if !matched { 23 | t.Errorf("expected %q to contain decryption tag", result) 24 | } 25 | } 26 | 27 | func TestGoDecryptFunc(t *testing.T) { 28 | keystore := path.Clean("./test_keys") 29 | 30 | f := goDecryptFunc(keystore) 31 | 32 | result, err := f("MySql Password", "KAb40OjTPcnDZOwnkY5jQcTWrc2bA0Gen9WM2h4=", "f5qtnyK78Ac710T2", "myteamkey-2014-09-19") 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | expected := "kadjf454nkklz" 38 | if !reflect.DeepEqual(result, expected) { 39 | t.Errorf("expected %q to be %q", result, expected) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test_data/config.json: -------------------------------------------------------------------------------- 1 | This is a config file with a big secret: [gosecret|turtle data|I like turtles. Don't tell anyone.]. 2 | 3 | We are demonstrating that encrypted data can be stored within a config file and encrypted and decrypted using gosecret tools. 4 | 5 | connection_string: [gosecret|mongo db|mongo://fake:creds@127.0.0.1]. 6 | -------------------------------------------------------------------------------- /test_data/config_enc.json: -------------------------------------------------------------------------------- 1 | This is a config file with a big secret: [gosecret|turtle data|B1oX1+WtGF/yfIoYRAVUGmTkn5OBlGpdjtCtywclWYqUAeAEEO2T4JuO6tcdoaJRIOAL|t7+s82brBQ0U5bMd|myteamkey-2014-09-19]. 2 | 3 | We are demonstrating that encrypted data can be stored within a config file and encrypted and decrypted using gosecret tools. 4 | 5 | connection_string: [gosecret|mongo db|ZtRQbt6oLNMpuERuMKsGUd1z7jD22vfJQVHaINuTt6N/HAGBlbgHPD1pLbY=|kLTeR8rWW/sJOWcq|myteamkey-2014-09-19]. 6 | -------------------------------------------------------------------------------- /test_data/config_plaintext.json: -------------------------------------------------------------------------------- 1 | This is a config file with a big secret: I like turtles. Don't tell anyone.. 2 | 3 | We are demonstrating that encrypted data can be stored within a config file and encrypted and decrypted using gosecret tools. 4 | 5 | connection_string: mongo://fake:creds@127.0.0.1. 6 | -------------------------------------------------------------------------------- /test_data/config_special_characters.json: -------------------------------------------------------------------------------- 1 | This is a config file with a big secret: Sometimes we need to [gosecret|special characters|use characters like % and not freak out. G%a%c%!%K%H%F is fun.] 2 | -------------------------------------------------------------------------------- /test_data/config_special_characters_plaintext.json: -------------------------------------------------------------------------------- 1 | This is a config file with a big secret: Sometimes we need to use characters like % and not freak out. G%a%c%!%K%H%F is fun. 2 | -------------------------------------------------------------------------------- /test_data/nested/config.xml: -------------------------------------------------------------------------------- 1 | 2 | artifactory 3 | 4 | [gosecret|hunter5|worst password ever] 5 | 6 | 7 | -------------------------------------------------------------------------------- /test_data/password: -------------------------------------------------------------------------------- 1 | [gosecret|awesome_service_consumer_secret|mszn3eGDbLfgpyXAEQE4] 2 | -------------------------------------------------------------------------------- /test_data/template/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "dbpassword" : "{{goEncrypt "MySql Password" "kadjf454nkklz" "myteamkey-2014-09-19" }}" 3 | } 4 | -------------------------------------------------------------------------------- /test_data/template/config_hybrid.json: -------------------------------------------------------------------------------- 1 | { 2 | "dbpassword" : "{{goEncrypt "MySql Password" "kadjf454nkklz" "myteamkey-2014-09-19" }}", 3 | "dbpassword2": "[gosecret|MySql Password 2|kadjf454nkklz]" 4 | } 5 | -------------------------------------------------------------------------------- /test_data/template/encrypted.json: -------------------------------------------------------------------------------- 1 | { 2 | "dbpassword" : "{{goDecrypt "MySql Password" "PA9aGEFVYzHpXxJseVc4mjC45B0zGb3Qlr4m9b8=" "+N8VCOKvAJbGTrLH" "myteamkey-2014-09-19"}}" 3 | } 4 | -------------------------------------------------------------------------------- /test_data/template/encrypted_hybrid.json: -------------------------------------------------------------------------------- 1 | { 2 | "dbpassword" : "{{goDecrypt "MySql Password" "rOHlwyawalfWnQuS07BtzqdenseHQCyOKvqdu5Q=" "Pfu/CMkW6veDjEuq" "myteamkey-2014-09-19"}}", 3 | "dbpassword2": "[gosecret|MySql Password 2|LVwaZfqOLKZQMKpZ85DTYlJDBA6dl8eL7Gcw0xY=|3tfbw3Lg8JHg3dVN|myteamkey-2014-09-19]" 4 | } 5 | -------------------------------------------------------------------------------- /test_data/template/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "dbpassword" : "kadjf454nkklz" 3 | } 4 | -------------------------------------------------------------------------------- /test_data/template/output_hybrid.json: -------------------------------------------------------------------------------- 1 | { 2 | "dbpassword" : "kadjf454nkklz", 3 | "dbpassword2": "kadjf454nkklz" 4 | } 5 | -------------------------------------------------------------------------------- /test_keys/myteamkey-2014-09-19: -------------------------------------------------------------------------------- 1 | D0yW87qvwNX0Bs7ny1HfvMeLqV0uXPEW3pyDhfynRaA= --------------------------------------------------------------------------------