Secret implementation of config. Supporting config merge (#2027)
Co-authored-by: Asim Aslam <asim@aslam.me>
This commit is contained in:
		| @@ -27,16 +27,17 @@ type Value interface { | |||||||
| } | } | ||||||
|  |  | ||||||
| type Options struct { | type Options struct { | ||||||
| 	// Is the value being read a secret? |  | ||||||
| 	// If true, the Config will try to decode it with `SecretKey` |  | ||||||
| 	Secret bool | 	Secret bool | ||||||
| } | } | ||||||
|  |  | ||||||
| // Option sets values in Options |  | ||||||
| type Option func(o *Options) | type Option func(o *Options) | ||||||
|  |  | ||||||
| func Secret(isSecret bool) Option { | func Secret(b bool) Option { | ||||||
| 	return func(o *Options) { | 	return func(o *Options) { | ||||||
| 		o.Secret = isSecret | 		o.Secret = b | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | type Secrets interface { | ||||||
|  | 	Config | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										70
									
								
								config/secrets/encryption.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								config/secrets/encryption.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | |||||||
|  | package secrets | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"crypto/aes" | ||||||
|  | 	"crypto/cipher" | ||||||
|  | 	"crypto/rand" | ||||||
|  | 	"encoding/hex" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // encrypt/decrypt functions are taken from https://www.melvinvivas.com/how-to-encrypt-and-decrypt-data-using-aes/ | ||||||
|  |  | ||||||
|  | func encrypt(stringToEncrypt string, key []byte) (string, error) { | ||||||
|  | 	plaintext := []byte(stringToEncrypt) | ||||||
|  |  | ||||||
|  | 	//Create a new Cipher Block from the key | ||||||
|  | 	block, err := aes.NewCipher(key) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	//Create a new GCM - https://en.wikipedia.org/wiki/Galois/Counter_Mode | ||||||
|  | 	//https://golang.org/pkg/crypto/cipher/#NewGCM | ||||||
|  | 	aesGCM, err := cipher.NewGCM(block) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	//Create a nonce. Nonce should be from GCM | ||||||
|  | 	nonce := make([]byte, aesGCM.NonceSize()) | ||||||
|  | 	if _, err = io.ReadFull(rand.Reader, nonce); err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	//Encrypt the data using aesGCM.Seal | ||||||
|  | 	//Since we don't want to save the nonce somewhere else in this case, we add it as a prefix to the encrypted data. The first nonce argument in Seal is the prefix. | ||||||
|  | 	ciphertext := aesGCM.Seal(nonce, nonce, plaintext, nil) | ||||||
|  | 	return fmt.Sprintf("%x", ciphertext), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func decrypt(encryptedString string, key []byte) (string, error) { | ||||||
|  | 	enc, _ := hex.DecodeString(encryptedString) | ||||||
|  |  | ||||||
|  | 	//Create a new Cipher Block from the key | ||||||
|  | 	block, err := aes.NewCipher(key) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	//Create a new GCM | ||||||
|  | 	aesGCM, err := cipher.NewGCM(block) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	//Get the nonce size | ||||||
|  | 	nonceSize := aesGCM.NonceSize() | ||||||
|  |  | ||||||
|  | 	//Extract the nonce from the encrypted data | ||||||
|  | 	nonce, ciphertext := enc[:nonceSize], enc[nonceSize:] | ||||||
|  |  | ||||||
|  | 	//Decrypt the data | ||||||
|  | 	plaintext, err := aesGCM.Open(nil, nonce, ciphertext, nil) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return fmt.Sprintf("%s", plaintext), nil | ||||||
|  | } | ||||||
							
								
								
									
										121
									
								
								config/secrets/secrets.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								config/secrets/secrets.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,121 @@ | |||||||
|  | package secrets | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/base64" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
|  |  | ||||||
|  | 	"github.com/micro/go-micro/v3/config" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // NewSecrets returns a config that encrypts values at rest | ||||||
|  | func NewSecrets(config config.Config, encryptionKey string) (config.Secrets, error) { | ||||||
|  | 	return newSecrets(config, encryptionKey) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type secretConf struct { | ||||||
|  | 	config        config.Config | ||||||
|  | 	encryptionKey string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func newSecrets(config config.Config, encryptionKey string) (*secretConf, error) { | ||||||
|  | 	return &secretConf{ | ||||||
|  | 		config:        config, | ||||||
|  | 		encryptionKey: encryptionKey, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *secretConf) Get(path string, options ...config.Option) (config.Value, error) { | ||||||
|  | 	val, err := c.config.Get(path, options...) | ||||||
|  | 	empty := config.NewJSONValue([]byte("null")) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return empty, err | ||||||
|  | 	} | ||||||
|  | 	var v interface{} | ||||||
|  | 	err = json.Unmarshal(val.Bytes(), &v) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return empty, err | ||||||
|  | 	} | ||||||
|  | 	v, err = convertElements(v, c.fromEncrypted) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return empty, err | ||||||
|  | 	} | ||||||
|  | 	dat, err := json.Marshal(v) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return empty, err | ||||||
|  | 	} | ||||||
|  | 	return config.NewJSONValue(dat), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *secretConf) Set(path string, val interface{}, options ...config.Option) error { | ||||||
|  | 	// marshal to JSON and back so we can iterate on the | ||||||
|  | 	// value without reflection | ||||||
|  | 	JSON, err := json.Marshal(val) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	var v interface{} | ||||||
|  | 	err = json.Unmarshal(JSON, &v) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	v, err = convertElements(v, c.toEncrypted) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	return c.config.Set(path, v) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *secretConf) Delete(path string, options ...config.Option) error { | ||||||
|  | 	return c.config.Delete(path, options...) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func convertElements(elem interface{}, conversionFunc func(elem interface{}) (interface{}, error)) (interface{}, error) { | ||||||
|  | 	switch m := elem.(type) { | ||||||
|  | 	case map[string]interface{}: | ||||||
|  | 		for k, v := range m { | ||||||
|  | 			conv, err := convertElements(v, conversionFunc) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return nil, err | ||||||
|  | 			} | ||||||
|  | 			m[k] = conv | ||||||
|  |  | ||||||
|  | 		} | ||||||
|  | 		return m, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return conversionFunc(elem) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *secretConf) toEncrypted(elem interface{}) (interface{}, error) { | ||||||
|  | 	dat, err := json.Marshal(elem) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	encrypted, err := encrypt(string(dat), []byte(c.encryptionKey)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("Failed to encrypt: %v", err) | ||||||
|  | 	} | ||||||
|  | 	return string(base64.StdEncoding.EncodeToString([]byte(encrypted))), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *secretConf) fromEncrypted(elem interface{}) (interface{}, error) { | ||||||
|  | 	s, ok := elem.(string) | ||||||
|  | 	if !ok { | ||||||
|  | 		// This bit decides if the Secrets implementation suppports nonencrypted values | ||||||
|  | 		// ie. we could do: | ||||||
|  | 		// return nil, fmt.Errorf("Encrypted values should be strings, but got: %v", elem) | ||||||
|  | 		// but let's go with not making nonencrypted values blow up the whole thing | ||||||
|  | 		return elem, nil | ||||||
|  | 	} | ||||||
|  | 	dec, err := base64.StdEncoding.DecodeString(s) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return elem, nil | ||||||
|  | 	} | ||||||
|  | 	decrypted, err := decrypt(string(dec), []byte(c.encryptionKey)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return elem, nil | ||||||
|  | 	} | ||||||
|  | 	var ret interface{} | ||||||
|  | 	return ret, json.Unmarshal([]byte(decrypted), &ret) | ||||||
|  | } | ||||||
| @@ -1,6 +1,9 @@ | |||||||
| package storeconfig | package store | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
| 	"github.com/micro/go-micro/v3/config" | 	"github.com/micro/go-micro/v3/config" | ||||||
| 	"github.com/micro/go-micro/v3/store" | 	"github.com/micro/go-micro/v3/store" | ||||||
| ) | ) | ||||||
| @@ -22,16 +25,6 @@ func newConfig(store store.Store, key string) (*conf, error) { | |||||||
| 	}, nil | 	}, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func mergeOptions(old config.Options, nu ...config.Option) config.Options { |  | ||||||
| 	n := config.Options{ |  | ||||||
| 		Secret: old.Secret, |  | ||||||
| 	} |  | ||||||
| 	for _, opt := range nu { |  | ||||||
| 		opt(&n) |  | ||||||
| 	} |  | ||||||
| 	return n |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (c *conf) Get(path string, options ...config.Option) (config.Value, error) { | func (c *conf) Get(path string, options ...config.Option) (config.Value, error) { | ||||||
| 	rec, err := c.store.Read(c.key) | 	rec, err := c.store.Read(c.key) | ||||||
| 	dat := []byte("{}") | 	dat := []byte("{}") | ||||||
| @@ -49,13 +42,56 @@ func (c *conf) Set(path string, val interface{}, options ...config.Option) error | |||||||
| 		dat = rec[0].Value | 		dat = rec[0].Value | ||||||
| 	} | 	} | ||||||
| 	values := config.NewJSONValues(dat) | 	values := config.NewJSONValues(dat) | ||||||
|  |  | ||||||
|  | 	// marshal to JSON and back so we can iterate on the | ||||||
|  | 	// value without reflection | ||||||
|  | 	// @todo only do this if a struct | ||||||
|  | 	JSON, err := json.Marshal(val) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	var v interface{} | ||||||
|  | 	err = json.Unmarshal(JSON, &v) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	m, ok := v.(map[string]interface{}) | ||||||
|  | 	if ok { | ||||||
|  | 		err := traverse(m, []string{path}, func(p string, value interface{}) error { | ||||||
|  | 			values.Set(p, value) | ||||||
|  | 			return nil | ||||||
|  | 		}) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
| 		values.Set(path, val) | 		values.Set(path, val) | ||||||
|  | 	} | ||||||
| 	return c.store.Write(&store.Record{ | 	return c.store.Write(&store.Record{ | ||||||
| 		Key:   c.key, | 		Key:   c.key, | ||||||
| 		Value: values.Bytes(), | 		Value: values.Bytes(), | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func traverse(m map[string]interface{}, paths []string, callback func(path string, value interface{}) error) error { | ||||||
|  | 	for k, v := range m { | ||||||
|  | 		val, ok := v.(map[string]interface{}) | ||||||
|  | 		if !ok { | ||||||
|  | 			err := callback(strings.Join(append(paths, k), "."), v) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		err := traverse(val, append(paths, k), callback) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
| func (c *conf) Delete(path string, options ...config.Option) error { | func (c *conf) Delete(path string, options ...config.Option) error { | ||||||
| 	rec, err := c.store.Read(c.key) | 	rec, err := c.store.Read(c.key) | ||||||
| 	dat := []byte("{}") | 	dat := []byte("{}") | ||||||
|   | |||||||
							
								
								
									
										119
									
								
								config/store/store_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								config/store/store_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,119 @@ | |||||||
|  | package store | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"reflect" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"github.com/micro/go-micro/v3/config" | ||||||
|  | 	"github.com/micro/go-micro/v3/config/secrets" | ||||||
|  | 	"github.com/micro/go-micro/v3/store/memory" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type conf1 struct { | ||||||
|  | 	A string  `json:"a"` | ||||||
|  | 	B int64   `json:"b"` | ||||||
|  | 	C float64 `json:"c"` | ||||||
|  | 	D bool    `json:"d"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestBasics(t *testing.T) { | ||||||
|  | 	conf, err := NewConfig(memory.NewStore(), "micro") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	testBasics(conf, t) | ||||||
|  | 	// We need to get a new config because existing config so | ||||||
|  | 	conf, err = NewConfig(memory.NewStore(), "micro1") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	secrets, err := secrets.NewSecrets(conf, "somethingRandomButLongEnough32by") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	testBasics(secrets, t) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func testBasics(c config.Config, t *testing.T) { | ||||||
|  | 	original := &conf1{ | ||||||
|  | 		"Hi", int64(42), float64(42.2), true, | ||||||
|  | 	} | ||||||
|  | 	err := c.Set("key", original) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	getted := &conf1{} | ||||||
|  | 	val, err := c.Get("key") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	err = val.Scan(getted) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	if !reflect.DeepEqual(original, getted) { | ||||||
|  | 		t.Fatalf("Not equal: %v and %v", original, getted) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Testing merges now | ||||||
|  | 	err = c.Set("key", map[string]interface{}{ | ||||||
|  | 		"b": 55, | ||||||
|  | 		"e": map[string]interface{}{ | ||||||
|  | 			"e1": true, | ||||||
|  | 		}, | ||||||
|  | 	}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	m := map[string]interface{}{} | ||||||
|  | 	val, err = c.Get("key") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	err = val.Scan(&m) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	expected := map[string]interface{}{ | ||||||
|  | 		"a": "Hi", | ||||||
|  | 		"b": float64(55), | ||||||
|  | 		"c": float64(42.2), | ||||||
|  | 		"d": true, | ||||||
|  | 		"e": map[string]interface{}{ | ||||||
|  | 			"e1": true, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	if !reflect.DeepEqual(m, expected) { | ||||||
|  | 		t.Fatalf("Not equal: %v and %v", m, expected) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Set just one value | ||||||
|  | 	expected = map[string]interface{}{ | ||||||
|  | 		"a": "Hi", | ||||||
|  | 		"b": float64(55), | ||||||
|  | 		"c": float64(42.2), | ||||||
|  | 		"d": true, | ||||||
|  | 		"e": map[string]interface{}{ | ||||||
|  | 			"e1": float64(45), | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	err = c.Set("key.e.e1", 45) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	m = map[string]interface{}{} | ||||||
|  | 	val, err = c.Get("key") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	err = val.Scan(&m) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if !reflect.DeepEqual(m, expected) { | ||||||
|  | 		t.Fatalf("Not equal: %v and %v", m, expected) | ||||||
|  | 	} | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user