├── .gitignore ├── LICENSE ├── README.md ├── backend ├── backend.go ├── consul │ └── consul.go ├── etcd │ └── etcd.go └── mock │ └── mock.go ├── bin └── crypt │ ├── README.md │ ├── cmd.go │ └── main.go ├── config ├── README.md ├── config.go └── config_test.go └── encoding └── secconf ├── keyring_test.go ├── secconf.go └── secconf_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | .pubring.gpg 27 | .secring.gpg 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 XOR Data Exchange, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # crypt 2 | 3 | You can use crypt as a command line tool or as a configuration library: 4 | 5 | * [crypt cli](bin/crypt) 6 | * [crypt/config](config) 7 | 8 | ## Demo 9 | 10 | Watch Kelsey explain `crypt` in this quick 5 minute video: 11 | 12 | [![Crypt Demonstration Video](https://img.youtube.com/vi/zYpqqfuGwW8/0.jpg)](https://www.youtube.com/watch?v=zYpqqfuGwW8) 13 | 14 | ## Generating gpg keys and keyrings 15 | 16 | The crypt cli and config package require gpg keyrings. 17 | 18 | ### Create a key and keyring from a batch file 19 | 20 | ``` 21 | vim app.batch 22 | ``` 23 | 24 | ``` 25 | %echo Generating a configuration OpenPGP key 26 | Key-Type: default 27 | Subkey-Type: default 28 | Name-Real: app 29 | Name-Comment: app configuration key 30 | Name-Email: app@example.com 31 | Expire-Date: 0 32 | %pubring .pubring.gpg 33 | %secring .secring.gpg 34 | %commit 35 | %echo done 36 | ``` 37 | 38 | Run the following command: 39 | 40 | ``` 41 | gpg2 --batch --armor --gen-key app.batch 42 | ``` 43 | 44 | You should now have two keyrings, `.pubring.gpg` which contains the public keys, and `.secring.gpg` which contains the private keys. 45 | 46 | > Note the private key is not protected by a passphrase. 47 | -------------------------------------------------------------------------------- /backend/backend.go: -------------------------------------------------------------------------------- 1 | // Package backend provides the K/V store interface for crypt backends. 2 | package backend 3 | 4 | // Response represents a response from a backend store. 5 | type Response struct { 6 | Value []byte 7 | Error error 8 | } 9 | 10 | // KVPair holds both a key and value when reading a list. 11 | type KVPair struct { 12 | Key string 13 | Value []byte 14 | } 15 | 16 | type KVPairs []*KVPair 17 | 18 | // A Store is a K/V store backend that retrieves and sets, and monitors 19 | // data in a K/V store. 20 | type Store interface { 21 | // Get retrieves a value from a K/V store for the provided key. 22 | Get(key string) ([]byte, error) 23 | 24 | // List retrieves all keys and values under a provided key. 25 | List(key string) (KVPairs, error) 26 | 27 | // Set sets the provided key to value. 28 | Set(key string, value []byte) error 29 | 30 | // Watch monitors a K/V store for changes to key. 31 | Watch(key string, stop chan bool) <-chan *Response 32 | } 33 | -------------------------------------------------------------------------------- /backend/consul/consul.go: -------------------------------------------------------------------------------- 1 | package consul 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | "github.com/xordataexchange/crypt/backend" 9 | 10 | "github.com/armon/consul-api" 11 | ) 12 | 13 | type Client struct { 14 | client *consulapi.KV 15 | waitIndex uint64 16 | } 17 | 18 | func New(machines []string) (*Client, error) { 19 | conf := consulapi.DefaultConfig() 20 | if len(machines) > 0 { 21 | conf.Address = machines[0] 22 | } 23 | client, err := consulapi.NewClient(conf) 24 | if err != nil { 25 | return nil, err 26 | } 27 | return &Client{client.KV(), 0}, nil 28 | } 29 | 30 | func (c *Client) Get(key string) ([]byte, error) { 31 | kv, _, err := c.client.Get(key, nil) 32 | if err != nil { 33 | return nil, err 34 | } 35 | if kv == nil { 36 | return nil, fmt.Errorf("Key ( %s ) was not found.", key) 37 | } 38 | return kv.Value, nil 39 | } 40 | 41 | func (c *Client) List(key string) (backend.KVPairs, error) { 42 | pairs, _, err := c.client.List(key, nil) 43 | if err != nil { 44 | return nil, err 45 | } 46 | if err != nil { 47 | return nil, err 48 | } 49 | ret := make(backend.KVPairs, len(pairs), len(pairs)) 50 | for i, kv := range pairs { 51 | ret[i] = &backend.KVPair{Key: kv.Key, Value: kv.Value} 52 | } 53 | return ret, nil 54 | } 55 | 56 | func (c *Client) Set(key string, value []byte) error { 57 | key = strings.TrimPrefix(key, "/") 58 | kv := &consulapi.KVPair{ 59 | Key: key, 60 | Value: value, 61 | } 62 | _, err := c.client.Put(kv, nil) 63 | return err 64 | } 65 | 66 | func (c *Client) Watch(key string, stop chan bool) <-chan *backend.Response { 67 | respChan := make(chan *backend.Response, 0) 68 | go func() { 69 | for { 70 | opts := consulapi.QueryOptions{ 71 | WaitIndex: c.waitIndex, 72 | } 73 | keypair, meta, err := c.client.Get(key, &opts) 74 | if keypair == nil && err == nil { 75 | err = fmt.Errorf("Key ( %s ) was not found.", key) 76 | } 77 | if err != nil { 78 | respChan <- &backend.Response{nil, err} 79 | time.Sleep(time.Second * 5) 80 | continue 81 | } 82 | c.waitIndex = meta.LastIndex 83 | respChan <- &backend.Response{keypair.Value, nil} 84 | } 85 | }() 86 | return respChan 87 | } 88 | -------------------------------------------------------------------------------- /backend/etcd/etcd.go: -------------------------------------------------------------------------------- 1 | package etcd 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/xordataexchange/crypt/backend" 10 | 11 | goetcd "github.com/coreos/etcd/client" 12 | ) 13 | 14 | type Client struct { 15 | client goetcd.Client 16 | keysAPI goetcd.KeysAPI 17 | waitIndex uint64 18 | } 19 | 20 | func New(machines []string) (*Client, error) { 21 | newClient, err := goetcd.New(goetcd.Config{ 22 | Endpoints: machines, 23 | }) 24 | if err != nil { 25 | return nil, fmt.Errorf("creating new etcd client for crypt.backend.Client: %v", err) 26 | } 27 | keysAPI := goetcd.NewKeysAPI(newClient) 28 | return &Client{client: newClient, keysAPI: keysAPI, waitIndex: 0}, nil 29 | } 30 | 31 | func (c *Client) Get(key string) ([]byte, error) { 32 | return c.GetWithContext(context.TODO(), key) 33 | } 34 | 35 | func (c *Client) GetWithContext(ctx context.Context, key string) ([]byte, error) { 36 | resp, err := c.keysAPI.Get(ctx, key, nil) 37 | if err != nil { 38 | return nil, err 39 | } 40 | return []byte(resp.Node.Value), nil 41 | } 42 | 43 | func addKVPairs(node *goetcd.Node, list backend.KVPairs) backend.KVPairs { 44 | if node.Dir { 45 | for _, n := range node.Nodes { 46 | list = addKVPairs(n, list) 47 | } 48 | return list 49 | } 50 | return append(list, &backend.KVPair{Key: node.Key, Value: []byte(node.Value)}) 51 | } 52 | 53 | func (c *Client) List(key string) (backend.KVPairs, error) { 54 | return c.ListWithContext(context.TODO(), key) 55 | } 56 | 57 | func (c *Client) ListWithContext(ctx context.Context, key string) (backend.KVPairs, error) { 58 | resp, err := c.keysAPI.Get(ctx, key, nil) 59 | if err != nil { 60 | return nil, err 61 | } 62 | if !resp.Node.Dir { 63 | return nil, errors.New("key is not a directory") 64 | } 65 | list := addKVPairs(resp.Node, nil) 66 | return list, nil 67 | } 68 | 69 | func (c *Client) Set(key string, value []byte) error { 70 | return c.SetWithContext(context.TODO(), key, value) 71 | } 72 | 73 | func (c *Client) SetWithContext(ctx context.Context, key string, value []byte) error { 74 | _, err := c.keysAPI.Set(ctx, key, string(value), nil) 75 | return err 76 | } 77 | 78 | func (c *Client) Watch(key string, stop chan bool) <-chan *backend.Response { 79 | return c.WatchWithContext(context.Background(), key, stop) 80 | } 81 | 82 | func (c *Client) WatchWithContext(ctx context.Context, key string, stop chan bool) <-chan *backend.Response { 83 | respChan := make(chan *backend.Response, 0) 84 | go func() { 85 | watcher := c.keysAPI.Watcher(key, nil) 86 | ctx, cancel := context.WithCancel(ctx) 87 | go func() { 88 | <-stop 89 | cancel() 90 | }() 91 | for { 92 | var resp *goetcd.Response 93 | var err error 94 | // if c.waitIndex == 0 { 95 | // resp, err = c.client.Get(key, false, false) 96 | // if err != nil { 97 | // respChan <- &backend.Response{nil, err} 98 | // time.Sleep(time.Second * 5) 99 | // continue 100 | // } 101 | // c.waitIndex = resp.EtcdIndex 102 | // respChan <- &backend.Response{[]byte(resp.Node.Value), nil} 103 | // } 104 | // resp, err = c.client.Watch(key, c.waitIndex+1, false, nil, stop) 105 | resp, err = watcher.Next(ctx) 106 | if err != nil { 107 | respChan <- &backend.Response{nil, err} 108 | time.Sleep(time.Second * 5) 109 | continue 110 | } 111 | c.waitIndex = resp.Node.ModifiedIndex 112 | respChan <- &backend.Response{[]byte(resp.Node.Value), nil} 113 | } 114 | }() 115 | return respChan 116 | } 117 | -------------------------------------------------------------------------------- /backend/mock/mock.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "errors" 5 | "path" 6 | "strings" 7 | "time" 8 | 9 | "github.com/xordataexchange/crypt/backend" 10 | ) 11 | 12 | var mockedStore map[string][]byte 13 | 14 | type Client struct{} 15 | 16 | func New(machines []string) (*Client, error) { 17 | if mockedStore == nil { 18 | mockedStore = make(map[string][]byte, 2) 19 | } 20 | return &Client{}, nil 21 | } 22 | 23 | func (c *Client) Get(key string) ([]byte, error) { 24 | if v, ok := mockedStore[key]; ok { 25 | return v, nil 26 | } 27 | err := errors.New("Could not find key: " + key) 28 | return nil, err 29 | } 30 | 31 | func (c *Client) List(key string) (backend.KVPairs, error) { 32 | var list backend.KVPairs 33 | dir := path.Clean(key) + "/" 34 | for k, v := range mockedStore { 35 | if strings.HasPrefix(k, dir) { 36 | list = append(list, &backend.KVPair{Key: k, Value: v}) 37 | } 38 | } 39 | return list, nil 40 | } 41 | 42 | func (c *Client) Set(key string, value []byte) error { 43 | mockedStore[key] = value 44 | return nil 45 | } 46 | 47 | func (c *Client) Watch(key string, stop chan bool) <-chan *backend.Response { 48 | respChan := make(chan *backend.Response, 0) 49 | go func() { 50 | for { 51 | b, err := c.Get(key) 52 | if err != nil { 53 | respChan <- &backend.Response{nil, err} 54 | time.Sleep(time.Second * 5) 55 | continue 56 | } 57 | respChan <- &backend.Response{b, nil} 58 | } 59 | }() 60 | return respChan 61 | } 62 | -------------------------------------------------------------------------------- /bin/crypt/README.md: -------------------------------------------------------------------------------- 1 | # crypt 2 | 3 | ## Install 4 | 5 | ### Binary release 6 | 7 | ``` 8 | wget https://github.com/xordataexchange/crypt/releases/download/v0.0.1/crypt-0.0.1-linux-amd64 9 | mv crypt-0.0.1-linux-amd64 /usr/local/bin/crypt 10 | chmod +x /usr/local/bin/crypt 11 | ``` 12 | 13 | ### go install 14 | 15 | ``` 16 | go install github.com/xordataexchange/crypt/bin/crypt 17 | ``` 18 | 19 | ## Backends 20 | 21 | crypt supports etcd and consul as backends via the `-backend` flag. 22 | 23 | ## Usage 24 | 25 | ``` 26 | usage: crypt COMMAND [arg...] 27 | 28 | commands: 29 | get retrieve the value of a key 30 | set set the value of a key 31 | ``` 32 | 33 | ### Encrypted and set a value 34 | 35 | ``` 36 | usage: crypt set [args...] key file 37 | -backend="etcd": backend provider 38 | -endpoint="": backend url 39 | -keyring=".pubring.gpg": path to armored public keyring 40 | ``` 41 | 42 | Example: 43 | 44 | ``` 45 | crypt set -keyring pubring.gpg /app/config config.json 46 | ``` 47 | 48 | ### Retrieve and decrypted a value 49 | 50 | ``` 51 | usage: crypt get [args...] key 52 | -backend="etcd": backend provider 53 | -endpoint="": backend url 54 | -secret-keyring=".secring.gpg": path to armored secret keyring 55 | ``` 56 | 57 | Example: 58 | 59 | ``` 60 | crypt get -secret-keyring secring.gpg /app/config 61 | ``` 62 | 63 | ### Support for unencrypted values 64 | ``` 65 | crypt set -plaintext ... 66 | crypt get -plaintext ... 67 | ``` 68 | Crypt now has support for getting and setting plain unencrypted values, as 69 | a convenience. This was added to the backend libraries so it could be exposed 70 | in spf13/viper. Use the -plaintext flag to get or set a value without encryption. 71 | -------------------------------------------------------------------------------- /bin/crypt/cmd.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "os" 10 | 11 | "github.com/xordataexchange/crypt/backend" 12 | "github.com/xordataexchange/crypt/backend/consul" 13 | "github.com/xordataexchange/crypt/backend/etcd" 14 | "github.com/xordataexchange/crypt/encoding/secconf" 15 | ) 16 | 17 | func getCmd(flagset *flag.FlagSet) { 18 | flagset.Usage = func() { 19 | fmt.Fprintf(os.Stderr, "usage: %s get [args...] key\n", os.Args[0]) 20 | flagset.PrintDefaults() 21 | } 22 | flagset.StringVar(&secretKeyring, "secret-keyring", ".secring.gpg", "path to armored secret keyring") 23 | flagset.Parse(os.Args[2:]) 24 | key := flagset.Arg(0) 25 | if key == "" { 26 | flagset.Usage() 27 | os.Exit(1) 28 | } 29 | backendStore, err := getBackendStore(backendName, endpoint) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | if plaintext { 34 | value, err := getPlain(key, backendStore) 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | fmt.Printf("%s\n", value) 39 | return 40 | } 41 | value, err := getEncrypted(key, secretKeyring, backendStore) 42 | 43 | if err != nil { 44 | log.Fatal(err) 45 | } 46 | fmt.Printf("%s\n", value) 47 | } 48 | 49 | func getEncrypted(key, keyring string, store backend.Store) ([]byte, error) { 50 | var value []byte 51 | kr, err := os.Open(secretKeyring) 52 | if err != nil { 53 | return value, err 54 | } 55 | defer kr.Close() 56 | data, err := store.Get(key) 57 | if err != nil { 58 | return value, err 59 | } 60 | value, err = secconf.Decode(data, kr) 61 | if err != nil { 62 | return value, err 63 | } 64 | return value, err 65 | 66 | } 67 | 68 | func getPlain(key string, store backend.Store) ([]byte, error) { 69 | var value []byte 70 | data, err := store.Get(key) 71 | if err != nil { 72 | return value, err 73 | } 74 | return data, err 75 | } 76 | 77 | func listCmd(flagset *flag.FlagSet) { 78 | flagset.Usage = func() { 79 | fmt.Fprintf(os.Stderr, "usage: %s list [args...] key\n", os.Args[0]) 80 | flagset.PrintDefaults() 81 | } 82 | flagset.StringVar(&secretKeyring, "secret-keyring", ".secring.gpg", "path to armored secret keyring") 83 | flagset.Parse(os.Args[2:]) 84 | key := flagset.Arg(0) 85 | if key == "" { 86 | flagset.Usage() 87 | os.Exit(1) 88 | } 89 | backendStore, err := getBackendStore(backendName, endpoint) 90 | if err != nil { 91 | log.Fatal(err) 92 | } 93 | if plaintext { 94 | list, err := listPlain(key, backendStore) 95 | if err != nil { 96 | log.Fatal(err) 97 | } 98 | for _, kv := range list { 99 | fmt.Printf("%s: %s", kv.Key, kv.Value) 100 | } 101 | return 102 | } 103 | list, err := listEncrypted(key, secretKeyring, backendStore) 104 | 105 | if err != nil { 106 | log.Fatal(err) 107 | } 108 | for _, kv := range list { 109 | fmt.Printf("%s: %s", kv.Key, kv.Value) 110 | } 111 | } 112 | 113 | func listEncrypted(key, keyring string, store backend.Store) (backend.KVPairs, error) { 114 | kr, err := os.Open(secretKeyring) 115 | if err != nil { 116 | return nil, err 117 | } 118 | defer kr.Close() 119 | 120 | data, err := store.List(key) 121 | if err != nil { 122 | return nil, err 123 | } 124 | for i, kv := range data { 125 | data[i].Value, err = secconf.Decode(kv.Value, kr) 126 | kr.Seek(0, 0) 127 | if err != nil { 128 | return nil, err 129 | } 130 | } 131 | return data, err 132 | } 133 | 134 | func listPlain(key string, store backend.Store) (backend.KVPairs, error) { 135 | data, err := store.List(key) 136 | if err != nil { 137 | return nil, err 138 | } 139 | return data, err 140 | } 141 | 142 | func setCmd(flagset *flag.FlagSet) { 143 | flagset.Usage = func() { 144 | fmt.Fprintf(os.Stderr, "usage: %s set [args...] key file\n", os.Args[0]) 145 | flagset.PrintDefaults() 146 | } 147 | flagset.StringVar(&keyring, "keyring", ".pubring.gpg", "path to armored public keyring") 148 | flagset.Parse(os.Args[2:]) 149 | key := flagset.Arg(0) 150 | if key == "" { 151 | flagset.Usage() 152 | os.Exit(1) 153 | } 154 | data := flagset.Arg(1) 155 | if data == "" { 156 | flagset.Usage() 157 | os.Exit(1) 158 | } 159 | backendStore, err := getBackendStore(backendName, endpoint) 160 | if err != nil { 161 | log.Fatal(err) 162 | } 163 | d, err := ioutil.ReadFile(data) 164 | if err != nil { 165 | log.Fatal(err) 166 | } 167 | 168 | if plaintext { 169 | err := setPlain(key, backendStore, d) 170 | if err != nil { 171 | log.Fatal(err) 172 | return 173 | } 174 | return 175 | } 176 | err = setEncrypted(key, keyring, d, backendStore) 177 | if err != nil { 178 | log.Fatal(err) 179 | } 180 | return 181 | 182 | } 183 | func setPlain(key string, store backend.Store, d []byte) error { 184 | err := store.Set(key, d) 185 | return err 186 | 187 | } 188 | 189 | func setEncrypted(key, keyring string, d []byte, store backend.Store) error { 190 | kr, err := os.Open(keyring) 191 | if err != nil { 192 | return err 193 | } 194 | defer kr.Close() 195 | secureValue, err := secconf.Encode(d, kr) 196 | if err != nil { 197 | return err 198 | } 199 | err = store.Set(key, secureValue) 200 | return err 201 | } 202 | 203 | func getBackendStore(provider string, endpoint string) (backend.Store, error) { 204 | if endpoint == "" { 205 | switch provider { 206 | case "consul": 207 | endpoint = "127.0.0.1:8500" 208 | case "etcd": 209 | endpoint = "http://127.0.0.1:4001" 210 | } 211 | } 212 | machines := []string{endpoint} 213 | switch provider { 214 | case "etcd": 215 | return etcd.New(machines) 216 | case "consul": 217 | return consul.New(machines) 218 | default: 219 | return nil, errors.New("invalid backend " + provider) 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /bin/crypt/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | ) 9 | 10 | var flagset = flag.NewFlagSet("crypt", flag.ExitOnError) 11 | 12 | var ( 13 | data string 14 | backendName string 15 | key string 16 | keyring string 17 | endpoint string 18 | secretKeyring string 19 | plaintext bool 20 | machines []string 21 | ) 22 | 23 | func init() { 24 | flagset.StringVar(&backendName, "backend", "etcd", "backend provider") 25 | flagset.StringVar(&endpoint, "endpoint", "", "backend url") 26 | flagset.BoolVar(&plaintext, "plaintext", false, "skip encryption") 27 | } 28 | 29 | func main() { 30 | log.SetFlags(0) 31 | if len(os.Args) < 2 { 32 | help() 33 | } 34 | cmd := os.Args[1] 35 | switch cmd { 36 | case "set": 37 | setCmd(flagset) 38 | case "get": 39 | getCmd(flagset) 40 | case "list": 41 | listCmd(flagset) 42 | default: 43 | help() 44 | } 45 | } 46 | 47 | func help() { 48 | fmt.Fprintf(os.Stderr, "usage: %s COMMAND [arg...]", os.Args[0]) 49 | fmt.Fprintf(os.Stderr, "\n\n") 50 | fmt.Fprintf(os.Stderr, "commands:\n") 51 | fmt.Fprintf(os.Stderr, " get retrieve the value of a key\n") 52 | fmt.Fprintf(os.Stderr, " list retrieve all values under a key\n") 53 | fmt.Fprintf(os.Stderr, " set set the value of a key\n") 54 | fmt.Fprintf(os.Stderr, "\n") 55 | fmt.Fprintf(os.Stderr, "-plaintext don't encrypt or decrypt the values before storage or retrieval\n") 56 | 57 | os.Exit(1) 58 | } 59 | -------------------------------------------------------------------------------- /config/README.md: -------------------------------------------------------------------------------- 1 | # crypt/config 2 | 3 | ## Usage 4 | 5 | ### Get configuration from a backend 6 | 7 | ``` 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | "log" 13 | "os" 14 | 15 | "github.com/xordataexchange/crypt/config" 16 | ) 17 | 18 | var ( 19 | key = "/app/config" 20 | secretKeyring = ".secring.gpg" 21 | ) 22 | 23 | func main() { 24 | kr, err := os.Open(secretKeyring) 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | defer kr.Close() 29 | machines := []string{"http://127.0.0.1:4001"} 30 | cm, err := config.NewEtcdConfigManager(machines, kr) 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | value, err := cm.Get(key) 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | fmt.Printf("%s\n", value) 39 | } 40 | ``` 41 | 42 | ### Monitor backend for configuration changes 43 | 44 | ``` 45 | package main 46 | 47 | import ( 48 | "fmt" 49 | "log" 50 | "os" 51 | 52 | "github.com/xordataexchange/crypt/config" 53 | ) 54 | 55 | var ( 56 | key = "/app/config" 57 | secretKeyring = ".secring.gpg" 58 | ) 59 | 60 | func main() { 61 | kr, err := os.Open(secretKeyring) 62 | if err != nil { 63 | log.Fatal(err) 64 | } 65 | defer kr.Close() 66 | machines := []string{"http://127.0.0.1:4001"} 67 | cm, err := config.NewEtcdConfigManager(machines, kr) 68 | if err != nil { 69 | log.Fatal(err) 70 | } 71 | stop := make(chan bool, 0) 72 | resp := cm.Watch(key, stop) 73 | if err != nil { 74 | log.Fatal(err) 75 | } 76 | for { 77 | select { 78 | case r := <-resp: 79 | if r.Error != nil { 80 | fmt.Println(r.Error.Error()) 81 | } 82 | fmt.Printf("%s\n", r.Value) 83 | } 84 | } 85 | } 86 | ``` 87 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "io/ioutil" 7 | 8 | "github.com/xordataexchange/crypt/backend" 9 | "github.com/xordataexchange/crypt/backend/consul" 10 | "github.com/xordataexchange/crypt/backend/etcd" 11 | "github.com/xordataexchange/crypt/encoding/secconf" 12 | ) 13 | 14 | type KVPair struct { 15 | backend.KVPair 16 | } 17 | 18 | type KVPairs []*KVPair 19 | 20 | type configManager struct { 21 | keystore []byte 22 | store backend.Store 23 | } 24 | 25 | // A ConfigManager retrieves and decrypts configuration from a key/value store. 26 | type ConfigManager interface { 27 | Get(key string) ([]byte, error) 28 | List(key string) (KVPairs, error) 29 | Set(key string, value []byte) error 30 | Watch(key string, stop chan bool) <-chan *Response 31 | } 32 | 33 | type standardConfigManager struct { 34 | store backend.Store 35 | } 36 | 37 | func NewStandardConfigManager(client backend.Store) (ConfigManager, error) { 38 | return standardConfigManager{client}, nil 39 | } 40 | 41 | func NewConfigManager(client backend.Store, keystore io.Reader) (ConfigManager, error) { 42 | bytes, err := ioutil.ReadAll(keystore) 43 | if err != nil { 44 | return nil, err 45 | } 46 | return configManager{bytes, client}, nil 47 | } 48 | 49 | // NewStandardEtcdConfigManager returns a new ConfigManager backed by etcd. 50 | func NewStandardEtcdConfigManager(machines []string) (ConfigManager, error) { 51 | store, err := etcd.New(machines) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | return NewStandardConfigManager(store) 57 | } 58 | 59 | // NewStandardConsulConfigManager returns a new ConfigManager backed by consul. 60 | func NewStandardConsulConfigManager(machines []string) (ConfigManager, error) { 61 | store, err := consul.New(machines) 62 | if err != nil { 63 | return nil, err 64 | } 65 | return NewStandardConfigManager(store) 66 | } 67 | 68 | // NewEtcdConfigManager returns a new ConfigManager backed by etcd. 69 | // Data will be encrypted. 70 | func NewEtcdConfigManager(machines []string, keystore io.Reader) (ConfigManager, error) { 71 | store, err := etcd.New(machines) 72 | if err != nil { 73 | return nil, err 74 | } 75 | return NewConfigManager(store, keystore) 76 | } 77 | 78 | // NewConsulConfigManager returns a new ConfigManager backed by consul. 79 | // Data will be encrypted. 80 | func NewConsulConfigManager(machines []string, keystore io.Reader) (ConfigManager, error) { 81 | store, err := consul.New(machines) 82 | if err != nil { 83 | return nil, err 84 | } 85 | return NewConfigManager(store, keystore) 86 | } 87 | 88 | // Get retrieves and decodes a secconf value stored at key. 89 | func (c configManager) Get(key string) ([]byte, error) { 90 | value, err := c.store.Get(key) 91 | if err != nil { 92 | return nil, err 93 | } 94 | return secconf.Decode(value, bytes.NewBuffer(c.keystore)) 95 | } 96 | 97 | // Get retrieves a value stored at key. 98 | // convenience function, no additional value provided over 99 | // `etcdctl` 100 | func (c standardConfigManager) Get(key string) ([]byte, error) { 101 | value, err := c.store.Get(key) 102 | if err != nil { 103 | return nil, err 104 | } 105 | return value, err 106 | } 107 | 108 | // List retrieves and decodes all secconf value stored under key. 109 | func (c configManager) List(key string) (KVPairs, error) { 110 | list, err := c.store.List(key) 111 | retList := make(KVPairs, len(list)) 112 | if err != nil { 113 | return nil, err 114 | } 115 | for i, kv := range list { 116 | retList[i].Key = kv.Key 117 | retList[i].Value, err = secconf.Decode(kv.Value, bytes.NewBuffer(c.keystore)) 118 | if err != nil { 119 | return nil, err 120 | } 121 | } 122 | return retList, nil 123 | } 124 | 125 | // List retrieves all values under key. 126 | // convenience function, no additional value provided over 127 | // `etcdctl` 128 | func (c standardConfigManager) List(key string) (KVPairs, error) { 129 | list, err := c.store.List(key) 130 | retList := make(KVPairs, len(list)) 131 | if err != nil { 132 | return nil, err 133 | } 134 | for i, kv := range list { 135 | retList[i].Key = kv.Key 136 | retList[i].Value = kv.Value 137 | } 138 | return retList, err 139 | } 140 | 141 | // Set will put a key/value into the data store 142 | // and encode it with secconf 143 | func (c configManager) Set(key string, value []byte) error { 144 | encodedValue, err := secconf.Encode(value, bytes.NewBuffer(c.keystore)) 145 | if err == nil { 146 | err = c.store.Set(key, encodedValue) 147 | } 148 | return err 149 | } 150 | 151 | // Set will put a key/value into the data store 152 | func (c standardConfigManager) Set(key string, value []byte) error { 153 | err := c.store.Set(key, value) 154 | return err 155 | } 156 | 157 | type Response struct { 158 | Value []byte 159 | Error error 160 | } 161 | 162 | func (c configManager) Watch(key string, stop chan bool) <-chan *Response { 163 | resp := make(chan *Response, 0) 164 | backendResp := c.store.Watch(key, stop) 165 | go func() { 166 | for { 167 | select { 168 | case <-stop: 169 | return 170 | case r := <-backendResp: 171 | if r.Error != nil { 172 | resp <- &Response{nil, r.Error} 173 | continue 174 | } 175 | value, err := secconf.Decode(r.Value, bytes.NewBuffer(c.keystore)) 176 | resp <- &Response{value, err} 177 | } 178 | } 179 | }() 180 | return resp 181 | } 182 | 183 | func (c standardConfigManager) Watch(key string, stop chan bool) <-chan *Response { 184 | resp := make(chan *Response, 0) 185 | backendResp := c.store.Watch(key, stop) 186 | go func() { 187 | for { 188 | select { 189 | case <-stop: 190 | return 191 | case r := <-backendResp: 192 | if r.Error != nil { 193 | resp <- &Response{nil, r.Error} 194 | continue 195 | } 196 | resp <- &Response{r.Value, nil} 197 | } 198 | } 199 | }() 200 | return resp 201 | } 202 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bytes" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/xordataexchange/crypt/backend/mock" 9 | ) 10 | 11 | var pubring = `-----BEGIN PGP PUBLIC KEY BLOCK----- 12 | Version: GnuPG/MacGPG2 v2.0.22 (Darwin) 13 | Comment: GPGTools - https://gpgtools.org 14 | 15 | mQENBFRClxIBCACjlm8e2mI5TmeigPuF4HJqNxc6AFLoCsE3MQ6VtdEVqidXZ06L 16 | m7uIXYc3IfvAlID+1KrUJnO2IgLEMmXKSDI5aOch7VaeoXLKMt7yAX+N6DHaZl4l 17 | eUGlfyIuBGD7FY2rv4hHo2wOmlq/chnNA4T7wb2XzeaAjvvoxcedMZ2npVimjwsl 18 | MNDxSxYPlHR6lJgfYJHAxcWn7ZQJW2Kllv9jMQwzGqW4fxuKRhe20KStE/4+K9gL 19 | GWv6OoE2gcGLoXliIMchHobY0GEvVx+YUv5jAItRSXq4ajYjFLtsWLz6FYtK9CoO 20 | Va6T5EGqozKST/olW/FMmKLOTzpAilyoKB/HABEBAAG0LWFwcCAoYXBwIGNvbmZp 21 | Z3VyYXRpb24ga2V5KSA8YXBwQGV4YW1wbGUuY29tPokBNwQTAQoAIQUCVEKXEgIb 22 | AwULCQgHAwUVCgkICwUWAgMBAAIeAQIXgAAKCRA8TymBhIANsjB1CACi4kqqWNSq 23 | AID7LmMswh5FQDEPkI/WA0h75xead11FVSdvtjWANY4Wob8RBjeZNT0TaCa0IAoo 24 | k+tLqA5xNbbvalOPV2zfr86BcGMhIs900++PuVjOb7XaJPsEt5JwtzuLM+eDLIVh 25 | vMI7hQtgB39O8/AsWEW/E/JlVtHcrsQ7LfcQYmNZVSnL71a8w4G+A6Sto89fvpjY 26 | h9/M4+aHqMhO/NLLp8Ylj5TlyiWKHZlx5ufl2ejWMUot3wFhYADHPkhydmQV9IY1 27 | zzIpmB/75kvZqC4p92k7l8Ra82o+T75/dNy0HcgvgrfZQttxIM0WPEyVF5NjicSo 28 | akoggAAslhCNuQENBFRClxIBCADJltx4EgkFScH/EAmO6+mZb6+pcpjY/H97bX4w 29 | KUrQSDZjDAhoxsInKgqHwAo3QY261eYrAyHvoTA2kRAaVrYWeGu3RxMmX5LTjFsX 30 | IW44ocTJK1XziUQympgIEayOUHt+XJaMGL8RKXvNgttGkr2VPD0IWJCOaBr8ZxUG 31 | Fm/pRFeBe6tX02RVKx4QFPqCnb76bkvR1cNeFsV5eEz0WNRYzena+lD6Oqh074tk 32 | oC9Uwl7D0l7xq17HNqAqHdMIO/T/TMPYyb7vskHPL9g8EJSgU55Z2Cjx3IlbJCpA 33 | 204cbbak4h99kgAqb4O5jT3gDe03NzWXCeQVKmacahusqNxzABEBAAGJAR8EGAEK 34 | AAkFAlRClxICGwwACgkQPE8pgYSADbJFTwf/d6XIv8/BxaP5TB12FxRXevOVenYe 35 | xR6O0rXDKFkP54LHSh2uaJbjLHevF0tuiNLFWq9RPhVjC012OLdo+2ygEFwNfikd 36 | 1SMbUIkuZ6Nu2MvCdrpAwbcvLgeoe8bqf1B6EIb31/OxCmtHujpVw6cSAnpAVyYo 37 | PjPtEpcNatIHbOna4KANxwv1Rmc6XDxWIB0RIlbdZDUhEdLovLLWGjm4J++Cnn2n 38 | OFdZyyUxwYTjDCMWwsYrG2oPZ0Yle6fKEXX30E+dN9NSV1i+dJAYQi0am6augpg+ 39 | LmFWxQ6JPmUJVDay9wo6g2D4KbJQybSh8lmqpenHnKD1m/gCGadPmMl6Rw== 40 | =FKbO 41 | -----END PGP PUBLIC KEY BLOCK-----` 42 | 43 | var secring = ` 44 | -----BEGIN PGP PRIVATE KEY BLOCK----- 45 | Version: GnuPG/MacGPG2 v2.0.22 (Darwin) 46 | Comment: GPGTools - https://gpgtools.org 47 | 48 | lQOYBFRClxIBCACjlm8e2mI5TmeigPuF4HJqNxc6AFLoCsE3MQ6VtdEVqidXZ06L 49 | m7uIXYc3IfvAlID+1KrUJnO2IgLEMmXKSDI5aOch7VaeoXLKMt7yAX+N6DHaZl4l 50 | eUGlfyIuBGD7FY2rv4hHo2wOmlq/chnNA4T7wb2XzeaAjvvoxcedMZ2npVimjwsl 51 | MNDxSxYPlHR6lJgfYJHAxcWn7ZQJW2Kllv9jMQwzGqW4fxuKRhe20KStE/4+K9gL 52 | GWv6OoE2gcGLoXliIMchHobY0GEvVx+YUv5jAItRSXq4ajYjFLtsWLz6FYtK9CoO 53 | Va6T5EGqozKST/olW/FMmKLOTzpAilyoKB/HABEBAAEAB/wKvEBo68JJaiH2nJ9P 54 | qas92YVZMD9Al2rBoU2zOR4nsqW9SybGQP89aOHgQNyBcV1xG79lh8Eii+MsQUsZ 55 | IMQcV2GKV5sjyDWScQ8yHjNi5SuBs85sMs5s5XB2nkvyU6JF9J5QETicprgw2x84 56 | AIn1buvvGTs4vD6/h7Hcri5fRimBvh+dcH/48nXPH56cZEPl/53tJt/lWwlfFBX1 57 | phZMIPoHT1kihEt//Hn4raw30R/bm0CJP/PtiyRkNeebzJsIJXtzG30B8YZb6c/h 58 | TtobA4F4ZWtEEwotPBFtx4clS/+2amc+PY+ZGTKXjzvQChaz50gvtSUp9ns9X/G+ 59 | T/vRBADC3dNGE2Ut8DRE2C/MQ7DdZdHdxaHJSMV+08xI/OSDOxp3ea1S2cbjniIG 60 | cnuQ8ZXD4hWDKSZTGs2L4awdsL5eIhqACnxT3LXm0TBwBWDzE3CQZUQGc+2pFgDb 61 | 1Xc/By+OZgFCDlJhHuhK4Lf9EsH3HbV/Cmn8sDD+dKazLxUF1wQA1uiH8X/8dgcQ 62 | uH/RSH2C7+Sr2B2Tpha9kngg4/cB31v3YaBV2t55zBvhSObxCM97gl6FadrEjJsw 63 | FvN04DMWhlt2xWbLnt1v4suVo8V1Are4vqP8G/mWhJou2Ps/65nsFqStNHMA+xjQ 64 | h8hAqY/9Mmu9Vm6WNRON0WCT3Snil5ED/0zUGI2qogw35Uzu448FrrYlh97kj3wu 65 | RzOZB/mty2pVj9eJO0z6E3C6sYLvbxrd8TyFzs4fTP7WlwG5FMJu/I4cEBqUJ/rr 66 | +ulSV/HH7zLpD6hWZbuRYhY8uskkVH50be4bb7MrXtoeDKrKfM4+BKf39QaBDNfI 67 | jD0Perf+Ll0aRBm0LWFwcCAoYXBwIGNvbmZpZ3VyYXRpb24ga2V5KSA8YXBwQGV4 68 | YW1wbGUuY29tPokBNwQTAQoAIQUCVEKXEgIbAwULCQgHAwUVCgkICwUWAgMBAAIe 69 | AQIXgAAKCRA8TymBhIANsjB1CACi4kqqWNSqAID7LmMswh5FQDEPkI/WA0h75xea 70 | d11FVSdvtjWANY4Wob8RBjeZNT0TaCa0IAook+tLqA5xNbbvalOPV2zfr86BcGMh 71 | Is900++PuVjOb7XaJPsEt5JwtzuLM+eDLIVhvMI7hQtgB39O8/AsWEW/E/JlVtHc 72 | rsQ7LfcQYmNZVSnL71a8w4G+A6Sto89fvpjYh9/M4+aHqMhO/NLLp8Ylj5TlyiWK 73 | HZlx5ufl2ejWMUot3wFhYADHPkhydmQV9IY1zzIpmB/75kvZqC4p92k7l8Ra82o+ 74 | T75/dNy0HcgvgrfZQttxIM0WPEyVF5NjicSoakoggAAslhCNnQOYBFRClxIBCADJ 75 | ltx4EgkFScH/EAmO6+mZb6+pcpjY/H97bX4wKUrQSDZjDAhoxsInKgqHwAo3QY26 76 | 1eYrAyHvoTA2kRAaVrYWeGu3RxMmX5LTjFsXIW44ocTJK1XziUQympgIEayOUHt+ 77 | XJaMGL8RKXvNgttGkr2VPD0IWJCOaBr8ZxUGFm/pRFeBe6tX02RVKx4QFPqCnb76 78 | bkvR1cNeFsV5eEz0WNRYzena+lD6Oqh074tkoC9Uwl7D0l7xq17HNqAqHdMIO/T/ 79 | TMPYyb7vskHPL9g8EJSgU55Z2Cjx3IlbJCpA204cbbak4h99kgAqb4O5jT3gDe03 80 | NzWXCeQVKmacahusqNxzABEBAAEAB/47pozhaLDLpEonz9aMOImckfxgPx00Y+7T 81 | FpC27pkJLb0OLPLWEi5ESX/pMG21cQvfw8iCZMBneIJcOyuRJ6Rk3Mg+6OSlP7Wi 82 | LI+NtiI31sJ0poKd+Dm6YZ1oEdbGG9GXEA2qMe5jxSsxoi2BYg2AOd1zeUV5JhwK 83 | IPSLIxuFYeDV/erv0n73Lob/Xj7SzhwRNQUJuG9Ak+maha1oqHwTuzPox9e+kSkK 84 | +VOhW+9oTukxsg8lCD351X/VvHeJgZkfTshLbQdAbMUlBQ00O7TyprFFLKcd0MNL 85 | gdVz5vHson5NyEzxsCbnV0Hty5Am00r1hm3Y89/k9HmBr3f+IH6JBADK0ZN9m4Br 86 | xpc2fou40/HBKBPk/5sJoOcHklBM7j4COYqloYaYliZRKmeWfH3gPhYW+EOqsZtv 87 | BPZaS7RL0IU8GoC1GfIrHJ+4GwiZQm6URDvEVSWsWiaUkI+cnK1HX8zsWHq48tqF 88 | yVSOZ05Lh3Id65s3mnXzF3/zzQLMmKm1OwQA/nLDZSMRdr/WWW2nFpf5QH0y9eI3 89 | VU/4/QSIBLFL5iAXOebHDseCr7/G/W6hn00VTQIUq3UKDi+gy9epm9aBrdNyF3Ey 90 | PvuACFLduF4ZnPOeZ1YrBxCRPHnGf+3So2Kcl9c1+RzMJ/qY+lZCU6pMCgCkeAZP 91 | iTGeuExKr9OrIikD/Au6yH+Oc2GEvorhoWcerEeXFvvx1S+9oJBKnJl9y6PRJacy 92 | wkZ354RyD9AojMJliibaHdAdpGSrOL8NEYQGy/3YzW1sMS2GBw6yZJ/GPCRDVEaE 93 | Nkbi/Aj3Shh2+w/jeYsUgrJkZY/UeoJt/mdUO1+loRoqTdlOOJLpPcyF6WzQQU+J 94 | AR8EGAEKAAkFAlRClxICGwwACgkQPE8pgYSADbJFTwf/d6XIv8/BxaP5TB12FxRX 95 | evOVenYexR6O0rXDKFkP54LHSh2uaJbjLHevF0tuiNLFWq9RPhVjC012OLdo+2yg 96 | EFwNfikd1SMbUIkuZ6Nu2MvCdrpAwbcvLgeoe8bqf1B6EIb31/OxCmtHujpVw6cS 97 | AnpAVyYoPjPtEpcNatIHbOna4KANxwv1Rmc6XDxWIB0RIlbdZDUhEdLovLLWGjm4 98 | J++Cnn2nOFdZyyUxwYTjDCMWwsYrG2oPZ0Yle6fKEXX30E+dN9NSV1i+dJAYQi0a 99 | m6augpg+LmFWxQ6JPmUJVDay9wo6g2D4KbJQybSh8lmqpenHnKD1m/gCGadPmMl6 100 | Rw== 101 | =RvPL 102 | -----END PGP PRIVATE KEY BLOCK----- 103 | ` 104 | 105 | func Test_StandardSet_BasePath(t *testing.T) { 106 | key := "foo" 107 | value := []byte("bar") 108 | 109 | store, err := mock.New([]string{}) 110 | if err != nil { 111 | t.Errorf("Error creating backend: %s\n", err.Error()) 112 | } 113 | cm, err := NewStandardConfigManager(store) 114 | if err != nil { 115 | t.Errorf("Error creating config manager: %s\n", err.Error()) 116 | } 117 | err = cm.Set(key, value) 118 | if err != nil { 119 | t.Errorf("Error adding key: %s\n", err.Error()) 120 | } 121 | } 122 | 123 | func Test_StandardGet_BasePath(t *testing.T) { 124 | key := "foo" 125 | value := []byte("bar") 126 | 127 | store, err := mock.New([]string{}) 128 | if err != nil { 129 | t.Errorf("Error creating backend: %s\n", err.Error()) 130 | } 131 | cm, err := NewStandardConfigManager(store) 132 | if err != nil { 133 | t.Errorf("Error creating config manager: %s\n", err.Error()) 134 | } 135 | storedValue, err := cm.Get(key) 136 | if err != nil { 137 | t.Errorf("Error getting key: %s\n", err.Error()) 138 | } 139 | if !reflect.DeepEqual(storedValue, value) { 140 | t.Errorf("Two values did not match: %s\n", err.Error()) 141 | } 142 | } 143 | 144 | func Test_StandardGet_AlternatePath_NoKey(t *testing.T) { 145 | key := "doesnotexist" 146 | 147 | store, err := mock.New([]string{}) 148 | if err != nil { 149 | t.Errorf("Error creating backend: %s\n", err.Error()) 150 | } 151 | cm, err := NewStandardConfigManager(store) 152 | if err != nil { 153 | t.Errorf("Error creating config manager: %s\n", err.Error()) 154 | } 155 | _, err = cm.Get(key) 156 | if err == nil { 157 | t.Errorf("Did not get expected error\n") 158 | } 159 | } 160 | 161 | func Test_StandardWatch_BasePath(t *testing.T) { 162 | key := "foo" 163 | 164 | store, err := mock.New([]string{}) 165 | if err != nil { 166 | t.Errorf("Error creating backend: %s\n", err.Error()) 167 | } 168 | cm, err := NewStandardConfigManager(store) 169 | if err != nil { 170 | t.Errorf("Error creating config manager: %s\n", err.Error()) 171 | } 172 | timeout := make(chan bool, 0) 173 | resp := cm.Watch(key, timeout) 174 | select { 175 | case r := <-resp: 176 | if r.Error != nil { 177 | t.Errorf("Error watching value: %s\n", r.Error.Error()) 178 | } 179 | } 180 | } 181 | 182 | func Test_Set_BasePath(t *testing.T) { 183 | key := "foo_enc" 184 | value := []byte("bar_enc") 185 | 186 | store, err := mock.New([]string{}) 187 | if err != nil { 188 | t.Errorf("Error creating backend: %s\n", err.Error()) 189 | } 190 | cm, err := NewConfigManager(store, bytes.NewBufferString(pubring)) 191 | if err != nil { 192 | t.Errorf("Error creating config manager: %s\n", err.Error()) 193 | } 194 | err = cm.Set(key, value) 195 | if err != nil { 196 | t.Errorf("Error adding key: %s\n", err.Error()) 197 | } 198 | } 199 | 200 | func Test_Get_BasePath(t *testing.T) { 201 | key := "foo_enc" 202 | value := []byte("bar_enc") 203 | 204 | store, err := mock.New([]string{}) 205 | if err != nil { 206 | t.Errorf("Error creating backend: %s\n", err.Error()) 207 | } 208 | cm, err := NewConfigManager(store, bytes.NewBufferString(secring)) 209 | if err != nil { 210 | t.Errorf("Error creating config manager: %s\n", err.Error()) 211 | } 212 | storedValue, err := cm.Get(key) 213 | if err != nil { 214 | t.Errorf("Error getting key: %s\n", err.Error()) 215 | } 216 | if !reflect.DeepEqual(storedValue, value) { 217 | t.Errorf("Two values did not match: %s\n", err.Error()) 218 | } 219 | } 220 | 221 | func Test_Get_AlternatePath_NoKey(t *testing.T) { 222 | key := "doesnotexist_enc" 223 | 224 | store, err := mock.New([]string{}) 225 | if err != nil { 226 | t.Errorf("Error creating backend: %s\n", err.Error()) 227 | } 228 | cm, err := NewConfigManager(store, bytes.NewBufferString(secring)) 229 | if err != nil { 230 | t.Errorf("Error creating config manager: %s\n", err.Error()) 231 | } 232 | _, err = cm.Get(key) 233 | if err == nil { 234 | t.Errorf("Did not get expected error\n") 235 | } 236 | } 237 | 238 | func Test_Watch_BasePath(t *testing.T) { 239 | key := "foo_enc" 240 | 241 | store, err := mock.New([]string{}) 242 | if err != nil { 243 | t.Errorf("Error creating backend: %s\n", err.Error()) 244 | } 245 | cm, err := NewConfigManager(store, bytes.NewBufferString(secring)) 246 | if err != nil { 247 | t.Errorf("Error creating config manager: %s\n", err.Error()) 248 | } 249 | timeout := make(chan bool, 0) 250 | resp := cm.Watch(key, timeout) 251 | select { 252 | case r := <-resp: 253 | if r.Error != nil { 254 | t.Errorf("Error watching value: %s\n", r.Error.Error()) 255 | } 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /encoding/secconf/keyring_test.go: -------------------------------------------------------------------------------- 1 | package secconf 2 | 3 | var pubring = `-----BEGIN PGP PUBLIC KEY BLOCK----- 4 | Version: GnuPG/MacGPG2 v2.0.22 (Darwin) 5 | Comment: GPGTools - https://gpgtools.org 6 | 7 | mQENBFRClxIBCACjlm8e2mI5TmeigPuF4HJqNxc6AFLoCsE3MQ6VtdEVqidXZ06L 8 | m7uIXYc3IfvAlID+1KrUJnO2IgLEMmXKSDI5aOch7VaeoXLKMt7yAX+N6DHaZl4l 9 | eUGlfyIuBGD7FY2rv4hHo2wOmlq/chnNA4T7wb2XzeaAjvvoxcedMZ2npVimjwsl 10 | MNDxSxYPlHR6lJgfYJHAxcWn7ZQJW2Kllv9jMQwzGqW4fxuKRhe20KStE/4+K9gL 11 | GWv6OoE2gcGLoXliIMchHobY0GEvVx+YUv5jAItRSXq4ajYjFLtsWLz6FYtK9CoO 12 | Va6T5EGqozKST/olW/FMmKLOTzpAilyoKB/HABEBAAG0LWFwcCAoYXBwIGNvbmZp 13 | Z3VyYXRpb24ga2V5KSA8YXBwQGV4YW1wbGUuY29tPokBNwQTAQoAIQUCVEKXEgIb 14 | AwULCQgHAwUVCgkICwUWAgMBAAIeAQIXgAAKCRA8TymBhIANsjB1CACi4kqqWNSq 15 | AID7LmMswh5FQDEPkI/WA0h75xead11FVSdvtjWANY4Wob8RBjeZNT0TaCa0IAoo 16 | k+tLqA5xNbbvalOPV2zfr86BcGMhIs900++PuVjOb7XaJPsEt5JwtzuLM+eDLIVh 17 | vMI7hQtgB39O8/AsWEW/E/JlVtHcrsQ7LfcQYmNZVSnL71a8w4G+A6Sto89fvpjY 18 | h9/M4+aHqMhO/NLLp8Ylj5TlyiWKHZlx5ufl2ejWMUot3wFhYADHPkhydmQV9IY1 19 | zzIpmB/75kvZqC4p92k7l8Ra82o+T75/dNy0HcgvgrfZQttxIM0WPEyVF5NjicSo 20 | akoggAAslhCNuQENBFRClxIBCADJltx4EgkFScH/EAmO6+mZb6+pcpjY/H97bX4w 21 | KUrQSDZjDAhoxsInKgqHwAo3QY261eYrAyHvoTA2kRAaVrYWeGu3RxMmX5LTjFsX 22 | IW44ocTJK1XziUQympgIEayOUHt+XJaMGL8RKXvNgttGkr2VPD0IWJCOaBr8ZxUG 23 | Fm/pRFeBe6tX02RVKx4QFPqCnb76bkvR1cNeFsV5eEz0WNRYzena+lD6Oqh074tk 24 | oC9Uwl7D0l7xq17HNqAqHdMIO/T/TMPYyb7vskHPL9g8EJSgU55Z2Cjx3IlbJCpA 25 | 204cbbak4h99kgAqb4O5jT3gDe03NzWXCeQVKmacahusqNxzABEBAAGJAR8EGAEK 26 | AAkFAlRClxICGwwACgkQPE8pgYSADbJFTwf/d6XIv8/BxaP5TB12FxRXevOVenYe 27 | xR6O0rXDKFkP54LHSh2uaJbjLHevF0tuiNLFWq9RPhVjC012OLdo+2ygEFwNfikd 28 | 1SMbUIkuZ6Nu2MvCdrpAwbcvLgeoe8bqf1B6EIb31/OxCmtHujpVw6cSAnpAVyYo 29 | PjPtEpcNatIHbOna4KANxwv1Rmc6XDxWIB0RIlbdZDUhEdLovLLWGjm4J++Cnn2n 30 | OFdZyyUxwYTjDCMWwsYrG2oPZ0Yle6fKEXX30E+dN9NSV1i+dJAYQi0am6augpg+ 31 | LmFWxQ6JPmUJVDay9wo6g2D4KbJQybSh8lmqpenHnKD1m/gCGadPmMl6Rw== 32 | =FKbO 33 | -----END PGP PUBLIC KEY BLOCK-----` 34 | 35 | var secring = ` 36 | -----BEGIN PGP PRIVATE KEY BLOCK----- 37 | Version: GnuPG/MacGPG2 v2.0.22 (Darwin) 38 | Comment: GPGTools - https://gpgtools.org 39 | 40 | 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 92 | Rw== 93 | =RvPL 94 | -----END PGP PRIVATE KEY BLOCK----- 95 | ` 96 | -------------------------------------------------------------------------------- /encoding/secconf/secconf.go: -------------------------------------------------------------------------------- 1 | // Package secconf implements secconf encoding as specified in the following 2 | // format: 3 | // 4 | // base64(gpg(gzip(data))) 5 | // 6 | package secconf 7 | 8 | import ( 9 | "bytes" 10 | "compress/gzip" 11 | "encoding/base64" 12 | "io" 13 | "io/ioutil" 14 | 15 | "golang.org/x/crypto/openpgp" 16 | ) 17 | 18 | // Deocde decodes data using the secconf codec. 19 | func Decode(data []byte, secertKeyring io.Reader) ([]byte, error) { 20 | decoder := base64.NewDecoder(base64.StdEncoding, bytes.NewBuffer(data)) 21 | entityList, err := openpgp.ReadArmoredKeyRing(secertKeyring) 22 | if err != nil { 23 | return nil, err 24 | } 25 | md, err := openpgp.ReadMessage(decoder, entityList, nil, nil) 26 | if err != nil { 27 | return nil, err 28 | } 29 | gzReader, err := gzip.NewReader(md.UnverifiedBody) 30 | if err != nil { 31 | return nil, err 32 | } 33 | defer gzReader.Close() 34 | bytes, err := ioutil.ReadAll(gzReader) 35 | if err != nil { 36 | return nil, err 37 | } 38 | return bytes, nil 39 | } 40 | 41 | // Encode encodes data to a base64 encoded using the secconf codec. 42 | // data is encrypted with all public keys found in the supplied keyring. 43 | func Encode(data []byte, keyring io.Reader) ([]byte, error) { 44 | entityList, err := openpgp.ReadArmoredKeyRing(keyring) 45 | if err != nil { 46 | return nil, err 47 | } 48 | buffer := new(bytes.Buffer) 49 | encoder := base64.NewEncoder(base64.StdEncoding, buffer) 50 | pgpWriter, err := openpgp.Encrypt(encoder, entityList, nil, nil, nil) 51 | if err != nil { 52 | return nil, err 53 | } 54 | gzWriter := gzip.NewWriter(pgpWriter) 55 | if _, err := gzWriter.Write(data); err != nil { 56 | return nil, err 57 | } 58 | if err := gzWriter.Close(); err != nil { 59 | return nil, err 60 | } 61 | if err := pgpWriter.Close(); err != nil { 62 | return nil, err 63 | } 64 | if err := encoder.Close(); err != nil { 65 | return nil, err 66 | } 67 | return buffer.Bytes(), nil 68 | } 69 | -------------------------------------------------------------------------------- /encoding/secconf/secconf_test.go: -------------------------------------------------------------------------------- 1 | package secconf 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | var encodingTests = []struct { 9 | in, out string 10 | }{ 11 | {"secret", "secret"}, 12 | } 13 | 14 | func TestEncoding(t *testing.T) { 15 | for _, tt := range encodingTests { 16 | encoded, err := Encode([]byte(tt.in), bytes.NewBufferString(pubring)) 17 | if err != nil { 18 | t.Errorf(err.Error()) 19 | } 20 | decoded, err := Decode(encoded, bytes.NewBufferString(secring)) 21 | if err != nil { 22 | t.Errorf(err.Error()) 23 | } 24 | if tt.out != string(decoded) { 25 | t.Errorf("want %s, got %s", tt.out, decoded) 26 | } 27 | } 28 | } 29 | --------------------------------------------------------------------------------