├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── cmd ├── get │ └── main.go ├── put │ └── main.go └── shared │ ├── config_test.go │ ├── cryptic.yml │ ├── encryptor.go │ ├── encryptor_test.go │ └── store.go ├── config ├── aes.go ├── config.go ├── db.go ├── encryptor.go ├── kdf.go ├── kms.go ├── redis.go └── store.go ├── encryptor ├── aes.go ├── aes_test.go ├── encrypted_data.go ├── encryptor.go ├── errors.go ├── gcm.go ├── gcm_test.go ├── kdf.go ├── kdf_test.go ├── kms.go ├── kms_integration_test.go ├── kms_test.go ├── nop_encryptor.go ├── nop_encryptor_test.go └── package.go ├── examples_test.go ├── package.go ├── store ├── db.go ├── db_test.go ├── errors.go ├── memory.go ├── memory_test.go ├── package.go ├── redis.go ├── redis_test.go └── store.go └── terraform └── kms.tf /.gitignore: -------------------------------------------------------------------------------- 1 | terraform.tfstate 2 | terraform.tfstate.backup 3 | cryptic.yml 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.5 5 | - 1.6 6 | - 1.7 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Dom Dwyer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/domodwyer/cryptic.svg?branch=master)](https://travis-ci.org/domodwyer/cryptic) [![GoDoc](https://godoc.org/github.com/domodwyer/cryptic?status.svg)](https://godoc.org/github.com/domodwyer/cryptic) 2 | 3 | > [!NOTE] 4 | > This filled a gap at the time, but now there are great systems that solve credential mangement - go use one of those instead! 5 | 6 |

7 | 8 |

9 |

10 | Manage API keys, passwords, certificates, etc. with infrastructure you already use. 11 |

12 |

13 | 14 | - Proven encryption, by default uses *AES-256* with *SHA-256* for integrity checks. 15 | - Supports multiple data stores - use infrastructure you already have. 16 | - No dependency hell: single binary to store a secret, one more to fetch. 17 | - Use [Amazon KMS](https://aws.amazon.com/kms/) key wrapping to further control access to sensitive information. 18 | - Super **simple** to use! 19 | 20 | # Usage 21 | Put a password somewhere: 22 | ``` 23 | ./put -name=ApiKey -value="be65d27ae088a0e03fd8e1331d90b01649464cb7" 24 | ``` 25 | 26 | Get a password back out somewhere else: 27 | ``` 28 | ./get -name=ApiKey 29 | ``` 30 | 31 | Or as part of a script (say, an environment variable): 32 | ``` 33 | export API_KEY=$(get -name=ApiKey) 34 | ``` 35 | 36 | # Installation 37 | Download a [release](https://github.com/domodwyer/cryptic/releases) for the binaries and get going straight away. 38 | 39 | Drop a simple YAML file in the same directory as the binary (`./cryptic.yml` or `/etc/cryptic/cryptic.yml` for a global configuration) to configure encryption and stores - below is a minimal example: 40 | 41 | ```yml 42 | Store: "db" 43 | Encryptor: "aes-gcm-pbkdf2" 44 | 45 | DB: 46 | Host: "127.0.0.1:3306" 47 | Name: "db-name" 48 | Username: "root" 49 | Password: "password" 50 | 51 | # When in any "pbkdf2" Encryptor mode, the Key parameter is hashed 52 | # 4096 times with SHA-512 and used as the key for AES-256 53 | 54 | AES: 55 | Key: "super-secret-key" 56 | ``` 57 | 58 | # Configuration 59 | Bellow are all the configurable options for Cryptic: 60 | ```yml 61 | # Store can be either 'redis' or 'db' 62 | Store: "db" 63 | 64 | DB: 65 | Host: "127.0.0.1:3306" 66 | Name: "db-name" 67 | Username: "root" 68 | Password: "password" 69 | Table: "secrets" 70 | KeyColumn: "name" 71 | ValueColumn: "data" 72 | 73 | Redis: 74 | Host: "127.0.0.1:6379" 75 | DbIndex: 0 76 | Password: "" 77 | ReadTimeout: "3s" 78 | WriteTimeout: "5s" 79 | MaxRetries: 0 80 | 81 | # Encryptor can be either 'aes-gcm-pbkdf2', 'aes-pbkdf2', 'aes' or 'kms' 82 | Encryptor: "kms" 83 | 84 | # AES key size must be 16, 24 or 32 chars if encryptor = 'aes' 85 | AES: 86 | Key: "changeme" 87 | HmacKey: "changeme" # only needed if encryptor = 'aes' 88 | 89 | # KMS uses AES-256 and SHA256 for HMAC 90 | KMS: 91 | KeyID: "427a117a-ac47-4c90-b7fe-b33fe1a7a241" 92 | Region: "eu-west-1" 93 | ``` 94 | 95 | # Database 96 | 97 | The database table is a simple key-value table, but **must** include a UNIQUE constraint on the key column. Below is a SQL snippet suitable for the default settings: 98 | 99 | ```sql 100 | CREATE TABLE `secrets` ( 101 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 102 | `name` varchar(255) NOT NULL DEFAULT '', 103 | `data` blob NOT NULL, 104 | PRIMARY KEY (`id`), 105 | UNIQUE KEY `idx_name` (`name`) 106 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 107 | ``` 108 | 109 | # Amazon KMS / Key Wrapping 110 | [Amazon KMS](https://aws.amazon.com/kms/) is a key-management service that provides key wrapping and auditing features (and more) that you can take advantage of to further secure your secrets. 111 | 112 | Using IAM roles you can control read access to only your production machines for example, or only your dev team, or perhaps only certain users. 113 | 114 | Cryptic gets a secure 512-bit key from KMS and uses that to encrypt your data. To decrypt, first the stored key is sent to KMS for decryption, and the result is used to decrypt the AES-256 encrypted secret locally - your encrypted secret can't be recovered without both KMS and your AES secret. 115 | 116 | Included is a [terraform](https://www.terraform.io/) configuration to generate a KMS key - `terraform apply` and it'll return a key ID such as `427a117a-ac47-4c90-b7fe-b33fe1a7a241` (or make it [manually](https://docs.aws.amazon.com/kms/latest/developerguide/create-keys.html)). 117 | 118 | Assuming you have the AWS CLI installed and credentials configured, all you need is to configure like above and go! 119 | 120 | # Library Usage / Source 121 | ``` 122 | go get -v github.com/domodwyer/cryptic 123 | ``` 124 | 125 | For an example of how to use the library, check out the `put` and `get` binaries - each are only 50 lines long! 126 | 127 | The library supports storage of binary secrets, though the CLI tools currently don't. Retries/backoff/circuit-breaking/etc is left to the library user. 128 | 129 | PR's welcome - please target to the `dev` branch. 130 | 131 | Oh, and **vendor this** and everything else if you value your sanity. 132 | 133 | # Testing 134 | Unit tests cover every aspect of the library, including integration tests. 135 | 136 | Don't run integration tests against production systems, they might go wild and ruin your day. 137 | 138 | For redis: `REDIS_HOST="localhost:6379" go test ./... -v -tags="integration"` 139 | 140 | For KMS: `AWS_REGION="eu-west-1" KMS_KEY_ID="" go test ./... -v -tags="awsintegration"` 141 | 142 | Or combine them for double the fun. 143 | 144 | # Credits 145 | 146 | The idea was largely taken from [credstash](https://github.com/fugue/credstash) - I just didn't want to install python + dependencies, and I wanted to use redis. Many of the same security implications mentioned on the credstash README apply to Cryptic too. 147 | 148 | # Improvements 149 | 150 | - More backends (S3/DynamoDB/memcached/MongoDB/etc) 151 | - Secret versioning/rotation/expiration 152 | - Support for pipelined requests to backends to reduce latency 153 | - Redis transactional existing-key check with `WATCH` 154 | -------------------------------------------------------------------------------- /cmd/get/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | 9 | "github.com/domodwyer/cryptic/cmd/shared" 10 | "github.com/domodwyer/cryptic/config" 11 | ) 12 | 13 | var name = flag.String("name", "", "secret name") 14 | 15 | func init() { 16 | flag.Parse() 17 | } 18 | 19 | func main() { 20 | if *name == "" { 21 | log.Print("required parameter missing") 22 | flag.PrintDefaults() 23 | os.Exit(1) 24 | } 25 | 26 | config := config.New() 27 | 28 | enc, err := shared.GetEncryptor(config) 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | 33 | backend, err := shared.GetStore(config) 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | 38 | data, err := backend.Get(*name) 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | 43 | plain, err := enc.Decrypt(data) 44 | if err != nil { 45 | log.Fatal(err) 46 | } 47 | 48 | fmt.Printf("%s", plain) 49 | } 50 | -------------------------------------------------------------------------------- /cmd/put/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "os" 7 | 8 | "github.com/domodwyer/cryptic/cmd/shared" 9 | "github.com/domodwyer/cryptic/config" 10 | ) 11 | 12 | var name = flag.String("name", "", "secret name") 13 | var data = flag.String("value", "", "secret value") 14 | 15 | func init() { 16 | flag.Parse() 17 | } 18 | 19 | func main() { 20 | if *name == "" || *data == "" { 21 | log.Print("required parameter missing") 22 | flag.PrintDefaults() 23 | os.Exit(1) 24 | } 25 | 26 | config := config.New() 27 | 28 | enc, err := shared.GetEncryptor(config) 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | 33 | backend, err := shared.GetStore(config) 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | 38 | e, err := enc.Encrypt([]byte(*data)) 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | 43 | if err := backend.Put(*name, e); err != nil { 44 | log.Fatal(err) 45 | } 46 | 47 | log.Print("OK") 48 | } 49 | -------------------------------------------------------------------------------- /cmd/shared/config_test.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | type mockConfig struct { 4 | store string 5 | encryptor string 6 | kmsKeyID string 7 | kmsRegion string 8 | aesKey string 9 | aesHmacKey string 10 | kdfKey string 11 | } 12 | 13 | func (m mockConfig) Store() string { 14 | return m.store 15 | } 16 | 17 | func (m mockConfig) Encryptor() string { 18 | return m.encryptor 19 | } 20 | 21 | func (m mockConfig) KMSKeyID() string { 22 | return m.kmsKeyID 23 | } 24 | 25 | func (m mockConfig) KMSRegion() string { 26 | return m.kmsRegion 27 | } 28 | 29 | func (m mockConfig) AESKey() string { 30 | return m.aesKey 31 | } 32 | 33 | func (m mockConfig) AESHmacKey() string { 34 | return m.aesHmacKey 35 | } 36 | 37 | func (m mockConfig) KDFKey() string { 38 | return m.kdfKey 39 | } 40 | -------------------------------------------------------------------------------- /cmd/shared/cryptic.yml: -------------------------------------------------------------------------------- 1 | Store: "redis" 2 | Encryptor: "aes-pbkdf2" 3 | 4 | AES: 5 | Key: "867a117a-ac47-4c90-b7fe-b33fe1a7a242" -------------------------------------------------------------------------------- /cmd/shared/encryptor.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/domodwyer/cryptic/config" 7 | "github.com/domodwyer/cryptic/encryptor" 8 | ) 9 | 10 | // GetEncryptor returns a concrete type that implements EncryptDecryptor based 11 | // on the configured Encryptor value in the config file. 12 | func GetEncryptor(config config.Encryptor) (encryptor.EncryptDecryptor, error) { 13 | switch config.Encryptor() { 14 | case "aes-pbkdf2": 15 | return encryptor.NewKDF([]byte(config.KDFKey())) 16 | 17 | case "aes": 18 | return encryptor.NewAES([]byte(config.AESKey()), []byte(config.AESHmacKey())) 19 | 20 | case "aes-gcm-pbkdf2": 21 | enc, err := encryptor.NewKDF([]byte(config.KDFKey())) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | // Set the encryption provider to AESGCM 27 | enc.Provider = func(key []byte) (encryptor.EncryptDecryptor, error) { 28 | return encryptor.NewAESGCM(key[:32]) 29 | } 30 | 31 | return enc, nil 32 | 33 | case "aes-gcm": 34 | return encryptor.NewAESGCM([]byte(config.AESKey())) 35 | 36 | case "kms": 37 | if config.KMSKeyID() == "" { 38 | return nil, errors.New("kms: No key ID set") 39 | } 40 | return encryptor.NewKMS(config.KMSKeyID(), config.KMSRegion()), nil 41 | 42 | default: 43 | return nil, errors.New("unknown decryptor") 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /cmd/shared/encryptor_test.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/domodwyer/cryptic/config" 7 | "github.com/domodwyer/cryptic/encryptor" 8 | ) 9 | 10 | func TestGetEncryptor_AES(t *testing.T) { 11 | tests := []struct { 12 | // Test description. 13 | name string 14 | // Parameters. 15 | config config.Encryptor 16 | // Expected results. 17 | wantErr error 18 | }{ 19 | { 20 | "AES", 21 | mockConfig{ 22 | encryptor: "aes", 23 | aesKey: "1234567890123456", 24 | aesHmacKey: "HMAC", 25 | }, 26 | nil, 27 | }, 28 | { 29 | "AES key too short", 30 | mockConfig{ 31 | encryptor: "aes", 32 | aesKey: "1", 33 | aesHmacKey: "HMAC", 34 | }, 35 | encryptor.ErrKeyTooShort, 36 | }, 37 | { 38 | "HMAC key too short", 39 | mockConfig{ 40 | encryptor: "aes", 41 | aesKey: "1234567890123456", 42 | aesHmacKey: "", 43 | }, 44 | encryptor.ErrHmacKeyTooShort, 45 | }, 46 | } 47 | for _, tt := range tests { 48 | got, err := GetEncryptor(tt.config) 49 | if err != tt.wantErr { 50 | t.Errorf("%q. getEncryptor() error = %v, wantErr %v", tt.name, err, tt.wantErr) 51 | continue 52 | } 53 | if err != nil { 54 | continue 55 | } 56 | 57 | if _, ok := got.(*encryptor.AESCTREncryptor); !ok { 58 | t.Errorf("%q. getEncryptor() not correct type", tt.name) 59 | } 60 | } 61 | } 62 | 63 | func TestGetEncryptor_KMS(t *testing.T) { 64 | tests := []struct { 65 | // Test description. 66 | name string 67 | // Parameters. 68 | config config.Encryptor 69 | // Expected results. 70 | wantErr bool 71 | }{ 72 | { 73 | "KMS", 74 | mockConfig{ 75 | encryptor: "kms", 76 | kmsKeyID: "keyID", 77 | kmsRegion: "eu-west-1", 78 | }, 79 | false, 80 | }, 81 | { 82 | "KMS no Key ID", 83 | mockConfig{ 84 | encryptor: "kms", 85 | kmsKeyID: "", 86 | kmsRegion: "eu-west-1", 87 | }, 88 | true, 89 | }, 90 | } 91 | for _, tt := range tests { 92 | got, err := GetEncryptor(tt.config) 93 | if (err != nil) != tt.wantErr { 94 | t.Errorf("%q. getEncryptor() error = %v, wantErr %v", tt.name, err, tt.wantErr) 95 | continue 96 | } 97 | if err != nil { 98 | continue 99 | } 100 | 101 | if _, ok := got.(*encryptor.KMS); !ok { 102 | t.Errorf("%q. getEncryptor() not correct type", tt.name) 103 | } 104 | } 105 | } 106 | 107 | func TestGetEncryptor_KDF(t *testing.T) { 108 | tests := []struct { 109 | // Test description. 110 | name string 111 | // Parameters. 112 | config config.Encryptor 113 | // Expected results. 114 | wantErr bool 115 | }{ 116 | { 117 | "KDF", 118 | mockConfig{ 119 | encryptor: "aes-pbkdf2", 120 | kdfKey: "ok", 121 | }, 122 | false, 123 | }, 124 | { 125 | "Key too short", 126 | mockConfig{ 127 | encryptor: "aes-pbkdf2", 128 | kdfKey: "", 129 | }, 130 | true, 131 | }, 132 | } 133 | for _, tt := range tests { 134 | got, err := GetEncryptor(tt.config) 135 | if (err != nil) != tt.wantErr { 136 | t.Errorf("%q. getEncryptor() error = %v, wantErr %v", tt.name, err, tt.wantErr) 137 | continue 138 | } 139 | if err != nil { 140 | continue 141 | } 142 | 143 | if _, ok := got.(*encryptor.KDF); !ok { 144 | t.Errorf("%q. getEncryptor() not correct type", tt.name) 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /cmd/shared/store.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | "fmt" 7 | 8 | "gopkg.in/redis.v4" 9 | 10 | "github.com/domodwyer/cryptic/config" 11 | "github.com/domodwyer/cryptic/store" 12 | 13 | // Import the MySQL driver 14 | _ "github.com/go-sql-driver/mysql" 15 | ) 16 | 17 | // GetStore returns a concrete type that implements store.Interface based on the 18 | // configured Store value in the config file. 19 | func GetStore(config config.Store) (store.Interface, error) { 20 | var backend store.Interface 21 | var err error 22 | 23 | switch config.Store() { 24 | case "db": 25 | // Compose the DSN 26 | dsn := fmt.Sprintf("%s:%s@tcp(%s)]/%s", 27 | config.DBUsername(), 28 | config.DBPassword(), 29 | config.DBHost(), 30 | config.DBName(), 31 | ) 32 | 33 | // Connect to DB 34 | db, err2 := sql.Open("mysql", dsn) 35 | if err2 != nil { 36 | return nil, err2 37 | } 38 | 39 | // Configure DB store 40 | opts := &store.DBOpts{ 41 | Table: config.DBTable(), 42 | Key: config.DBKeyColumn(), 43 | Value: config.DBValueColumn(), 44 | } 45 | 46 | backend, err = store.NewDB(db, opts) 47 | 48 | case "redis": 49 | backend = store.NewRedis(&redis.Options{ 50 | Addr: config.RedisHost(), 51 | Password: config.RedisPassword(), 52 | DB: config.RedisDbIndex(), 53 | MaxRetries: config.RedisMaxRetries(), 54 | ReadTimeout: config.RedisReadTimeout(), 55 | WriteTimeout: config.RedisWriteTimeout(), 56 | }) 57 | 58 | default: 59 | err = errors.New("unknown store") 60 | } 61 | 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | return backend, nil 67 | } 68 | -------------------------------------------------------------------------------- /config/aes.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "github.com/spf13/viper" 4 | 5 | // AES defines config getters for the AES Encryptor parameters. 6 | type AES interface { 7 | AESKey() string 8 | AESHmacKey() string 9 | } 10 | 11 | // AESKey returns the configured AES key. 12 | func (v viperStore) AESKey() string { 13 | return viper.GetString("AES.Key") 14 | } 15 | 16 | // AESHmacKey returns the configured HMAC key used by the AES encryptor. 17 | func (v viperStore) AESHmacKey() string { 18 | return viper.GetString("AES.HmacKey") 19 | } 20 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "log" 5 | "sync" 6 | 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | // Interface combines the Store and Encryptor interfaces 11 | type Interface interface { 12 | Store 13 | Encryptor 14 | } 15 | 16 | // Store defines the interface providing getters related to stores 17 | type Store interface { 18 | SelectedStore 19 | Redis 20 | DB 21 | } 22 | 23 | // Encryptor defines the interface providing getters related to encryptors 24 | type Encryptor interface { 25 | SelectedEncryptor 26 | KMS 27 | AES 28 | KDF 29 | } 30 | 31 | type viperStore struct{} 32 | 33 | var vs *viperStore 34 | var once sync.Once 35 | 36 | func init() { 37 | defaults := map[string]interface{}{ 38 | "Store": "redis", 39 | "Encryptor": "kms", 40 | 41 | // KMS config 42 | "KMS.KeyID": "", 43 | "KMS.Region": "eu-west-1", 44 | 45 | // AES config 46 | "AES.Key": "", 47 | "AES.HmacKey": "", 48 | 49 | // Redis store config 50 | "Redis.Host": "127.0.0.1:6379", 51 | "Redis.DbIndex": 0, 52 | "Redis.Password": "", 53 | "Redis.ReadTimeout": "3s", 54 | "Redis.WriteTimeout": "5s", 55 | "Redis.MaxRetries": 0, 56 | 57 | // DB store config 58 | "DB.Host": "127.0.0.1:3306", 59 | "DB.Username": "root", 60 | "DB.Password": "", 61 | "DB.Name": "cryptic", 62 | "DB.Table": "secrets", 63 | "DB.KeyColumn": "name", 64 | "DB.ValueColumn": "data", 65 | } 66 | 67 | // First match takes preference 68 | viper.AddConfigPath(".") 69 | viper.AddConfigPath("/etc/cryptic/") 70 | 71 | for k, v := range defaults { 72 | viper.SetDefault(k, v) 73 | } 74 | 75 | viper.SetConfigName("cryptic") 76 | 77 | if err := viper.ReadInConfig(); err != nil { 78 | if _, ok := err.(viper.UnsupportedConfigError); ok { 79 | log.Print("config: failed to load config file, using defaults") 80 | } else { 81 | log.Fatalf("config: init error (%v)", err) 82 | } 83 | } 84 | 85 | } 86 | 87 | // New returns a config accessor that implements Interface as singleton 88 | func New() Interface { 89 | newInstance := func() { 90 | vs = &viperStore{} 91 | } 92 | 93 | once.Do(newInstance) 94 | 95 | return vs 96 | } 97 | -------------------------------------------------------------------------------- /config/db.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "github.com/spf13/viper" 4 | 5 | // DB defines config getters for the database Store parameters. 6 | type DB interface { 7 | DBHost() string 8 | DBName() string 9 | DBTable() string 10 | DBUsername() string 11 | DBPassword() string 12 | DBKeyColumn() string 13 | DBValueColumn() string 14 | } 15 | 16 | // DBHost returns the configured database host (in the form of ip:port). 17 | func (v viperStore) DBHost() string { 18 | return viper.GetString("DB.Host") 19 | } 20 | 21 | // DBName returns the configured database name. 22 | func (v viperStore) DBName() string { 23 | return viper.GetString("DB.Name") 24 | } 25 | 26 | // DBTable returns the configured database table. 27 | func (v viperStore) DBTable() string { 28 | return viper.GetString("DB.Table") 29 | } 30 | 31 | // DBUsername returns the configured database username. 32 | func (v viperStore) DBUsername() string { 33 | return viper.GetString("DB.Username") 34 | } 35 | 36 | // DBPassword returns the configured database password. 37 | func (v viperStore) DBPassword() string { 38 | return viper.GetString("DB.Password") 39 | } 40 | 41 | // DBKeyColumn returns the configured 'lookup' column name. 42 | func (v viperStore) DBKeyColumn() string { 43 | return viper.GetString("DB.KeyColumn") 44 | } 45 | 46 | // DBValueColumn returns the configured 'value' column name. 47 | func (v viperStore) DBValueColumn() string { 48 | return viper.GetString("DB.ValueColumn") 49 | } 50 | -------------------------------------------------------------------------------- /config/encryptor.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | // SelectedEncryptor defines config getters for the Encryptor type. 10 | type SelectedEncryptor interface { 11 | Encryptor() string 12 | } 13 | 14 | // Encryptor returns the configued Encryptor type name. 15 | func (v viperStore) Encryptor() string { 16 | return strings.ToLower(viper.GetString("Encryptor")) 17 | } 18 | -------------------------------------------------------------------------------- /config/kdf.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "github.com/spf13/viper" 4 | 5 | // KDF defines the configuration options for PBKDF2 support 6 | type KDF interface { 7 | KDFKey() string 8 | } 9 | 10 | // KDFKey returns the configured KDF key. 11 | func (v viperStore) KDFKey() string { 12 | return viper.GetString("AES.Key") 13 | } 14 | -------------------------------------------------------------------------------- /config/kms.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "github.com/spf13/viper" 4 | 5 | // KMS defines config getters for the KMS Encryptor parameters. 6 | type KMS interface { 7 | KMSKeyID() string 8 | KMSRegion() string 9 | } 10 | 11 | // KMSKeyID returns the configured KMS key ID. 12 | func (v viperStore) KMSKeyID() string { 13 | return viper.GetString("KMS.KeyID") 14 | } 15 | 16 | // KMSRegion returns the configured AWS region used for calls to KMS. 17 | func (v viperStore) KMSRegion() string { 18 | return viper.GetString("KMS.Region") 19 | } 20 | -------------------------------------------------------------------------------- /config/redis.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | // Redis defines config getters for the Redis store parameters. 10 | type Redis interface { 11 | RedisHost() string 12 | RedisDbIndex() int 13 | RedisPassword() string 14 | RedisMaxRetries() int 15 | RedisReadTimeout() time.Duration 16 | RedisWriteTimeout() time.Duration 17 | } 18 | 19 | // RedisHost returns the configured redis hostname (in the format ip:port). 20 | func (v viperStore) RedisHost() string { 21 | return viper.GetString("Redis.Host") 22 | } 23 | 24 | // RedisDbIndex returns the configured redis database index. 25 | func (v viperStore) RedisDbIndex() int { 26 | return viper.GetInt("Redis.DbIndex") 27 | } 28 | 29 | // RedisPassword returns the configured redis password. 30 | func (v viperStore) RedisPassword() string { 31 | return viper.GetString("Redis.Password") 32 | } 33 | 34 | // RedisMaxRetries returns the configured maximum number of retries for redis 35 | // operations. 36 | func (v viperStore) RedisMaxRetries() int { 37 | return viper.GetInt("Redis.MaxRetries") 38 | } 39 | 40 | // RedisReadTimeout returns the configured redis read timeout. 41 | func (v viperStore) RedisReadTimeout() time.Duration { 42 | return viper.GetDuration("Redis.ReadTimeout") 43 | } 44 | 45 | // RedisWriteTimeout returns the configured redis write timeout. 46 | func (v viperStore) RedisWriteTimeout() time.Duration { 47 | return viper.GetDuration("Redis.WriteTimeout") 48 | } 49 | -------------------------------------------------------------------------------- /config/store.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | // SelectedStore defines config getters for the Store type. 10 | type SelectedStore interface { 11 | Store() string 12 | } 13 | 14 | // Store returns the configued Store type name. 15 | func (v viperStore) Store() string { 16 | return strings.ToLower(viper.GetString("Store")) 17 | } 18 | -------------------------------------------------------------------------------- /encryptor/aes.go: -------------------------------------------------------------------------------- 1 | package encryptor 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "crypto/hmac" 7 | "crypto/rand" 8 | "crypto/sha256" 9 | "io" 10 | ) 11 | 12 | // AESCTREncryptor provides AES encryption of secrets with SHA-256 used for 13 | // message authentication. 14 | type AESCTREncryptor struct { 15 | aesKey []byte 16 | hmacKey []byte 17 | block cipher.Block 18 | } 19 | 20 | // NewAES returns an initialised Encryptor using AES in CTR (counter) mode and 21 | // SHA-256 for message authentication. 22 | func NewAES(aesKey, hmacKey []byte) (*AESCTREncryptor, error) { 23 | if len(hmacKey) == 0 { 24 | return nil, ErrHmacKeyTooShort 25 | } 26 | 27 | block, err := aes.NewCipher(aesKey) 28 | if err != nil { 29 | return nil, ErrKeyTooShort 30 | } 31 | 32 | return &AESCTREncryptor{ 33 | aesKey: aesKey, 34 | hmacKey: hmacKey, 35 | block: block, 36 | }, nil 37 | } 38 | 39 | // Encrypt generates a unique IV for each encryption, and encrypts the 40 | // plain-text secret with the configured AES key. 41 | // 42 | // A HMAC is then generated using SHA256 with the configured HMAC key. 43 | func (e *AESCTREncryptor) Encrypt(secret []byte) (*EncryptedData, error) { 44 | ciphertext := make([]byte, aes.BlockSize+len(secret)) 45 | 46 | // Generate a random IV 47 | iv := ciphertext[:aes.BlockSize] 48 | if _, err := io.ReadFull(rand.Reader, iv); err != nil { 49 | // No entropy? You've got bigger problems 50 | return nil, err 51 | } 52 | 53 | stream := cipher.NewCTR(e.block, iv) 54 | stream.XORKeyStream(ciphertext[aes.BlockSize:], secret) 55 | 56 | // Generate our HMAC 57 | mac := hmac.New(sha256.New, e.hmacKey) 58 | mac.Write(ciphertext) 59 | 60 | return &EncryptedData{ 61 | Ciphertext: ciphertext, 62 | HMAC: mac.Sum(nil), 63 | Type: AESCTR, 64 | }, nil 65 | } 66 | 67 | // Decrypt checks data has been created previously by AESCTREncryptor, validates 68 | // the HMAC in constant time to prevent a timing side-channel attack (and detect 69 | // any corruption of the ciphertext), and decrypts the cipher-text, returning 70 | // the original plain-text. 71 | func (e *AESCTREncryptor) Decrypt(data *EncryptedData) ([]byte, error) { 72 | // Ensure we're operating on something that AESCTREncryptor encrypted, and 73 | // not data from something else 74 | if data.Type != AESCTR { 75 | return nil, ErrWrongType 76 | } 77 | 78 | mac := hmac.New(sha256.New, e.hmacKey) 79 | mac.Write(data.Ciphertext) 80 | 81 | // Ensure the HMAC matches what we were expecting, use constant time 82 | // comparison 83 | if !hmac.Equal(mac.Sum(nil), data.HMAC) { 84 | return nil, ErrInvalidHmac 85 | } 86 | 87 | // Extract the IV 88 | if len(data.Ciphertext) < aes.BlockSize { 89 | return nil, ErrInvalidCiphertext 90 | } 91 | 92 | iv := data.Ciphertext[:aes.BlockSize] 93 | buf := data.Ciphertext[aes.BlockSize:] 94 | 95 | stream := cipher.NewCTR(e.block, iv) 96 | stream.XORKeyStream(buf, buf) 97 | 98 | return buf, nil 99 | } 100 | -------------------------------------------------------------------------------- /encryptor/aes_test.go: -------------------------------------------------------------------------------- 1 | package encryptor 2 | 3 | import ( 4 | "bytes" 5 | "crypto/hmac" 6 | "crypto/sha256" 7 | "testing" 8 | ) 9 | 10 | // TestNewAES ensures the AESCTREncryptor struct is correctly initialised. 11 | func TestNewAES(t *testing.T) { 12 | tests := []struct { 13 | // Test description. 14 | name string 15 | // Parameters. 16 | aesKey []byte 17 | hmacKey []byte 18 | // Expected results. 19 | want AESCTREncryptor 20 | wantErr error 21 | }{ 22 | { 23 | "Correct", 24 | []byte("12345678901234567890123456789012"), 25 | []byte("SECRETSM8"), 26 | AESCTREncryptor{ 27 | aesKey: []byte("12345678901234567890123456789012"), 28 | hmacKey: []byte("SECRETSM8"), 29 | }, 30 | nil, 31 | }, 32 | { 33 | "AES key required", 34 | []byte{}, 35 | []byte("SECRETSM8"), 36 | AESCTREncryptor{}, 37 | ErrKeyTooShort, 38 | }, 39 | { 40 | "HMAC key required", 41 | []byte("12345678901234567890123456789012"), 42 | []byte{}, 43 | AESCTREncryptor{}, 44 | ErrHmacKeyTooShort, 45 | }, 46 | { 47 | "Error with wrong AES key length", 48 | []byte("short"), 49 | []byte("SECRETSM8"), 50 | AESCTREncryptor{}, 51 | ErrKeyTooShort, 52 | }, 53 | } 54 | for _, tt := range tests { 55 | got, err := NewAES(tt.aesKey, tt.hmacKey) 56 | 57 | if err != tt.wantErr { 58 | t.Errorf("%q. NewAES() error = %v, wantErr %v", tt.name, err, tt.wantErr) 59 | } 60 | 61 | if err != nil { 62 | continue 63 | } 64 | 65 | if bytes.Compare(tt.want.aesKey, got.aesKey) != 0 { 66 | t.Errorf("%q. NewAES() easKey = %v, want %v", tt.name, got.aesKey, tt.want.aesKey) 67 | } 68 | 69 | if bytes.Compare(tt.want.hmacKey, got.hmacKey) != 0 { 70 | t.Errorf("%q. NewAES() easKey = %v, want %v", tt.name, got.hmacKey, tt.want.hmacKey) 71 | } 72 | } 73 | } 74 | 75 | // TestAesCTREncryptorIntegration ensures Encrypt() and Decrypt() work together 76 | // to produce the same plain-text as the original input. 77 | func TestAESCTREncryptorIntegration(t *testing.T) { 78 | tests := []struct { 79 | // Test description. 80 | name string 81 | // Parameters. 82 | want []byte 83 | }{ 84 | { 85 | "Simple string", 86 | []byte("i am a secret"), 87 | }, 88 | { 89 | "Binary", 90 | []byte{ 91 | 0xb0, 0x75, 0x11, 0x62, 0xa2, 0x3e, 0x5f, 0x2f, 92 | 0xca, 0xa3, 0x00, 0x1d, 0x51, 0x89, 0xc8, 0xe7, 93 | 0xb5, 0x15, 0xb9, 0x5c, 0x9b, 0x3e, 0x26, 0x5f, 94 | 0xb2, 0x6b, 0x97, 0x41, 0x16, 0x2c, 0x47, 0x10, 95 | }, 96 | }, 97 | } 98 | 99 | for _, tt := range tests { 100 | e, err := NewAES([]byte("anAesTestKey1234"), []byte("hmacKey")) 101 | if err != nil { 102 | t.Errorf("%q. NewAES() = %s", tt.name, err) 103 | continue 104 | } 105 | 106 | encrypted, err := e.Encrypt(tt.want) 107 | if err != nil { 108 | t.Errorf("%q. Encrypt() = %s", tt.name, err) 109 | continue 110 | } 111 | 112 | got, err := e.Decrypt(encrypted) 113 | if err != nil { 114 | t.Errorf("%q. Decrypt() = %s", tt.name, err) 115 | continue 116 | } 117 | 118 | if !bytes.Equal(got, tt.want) { 119 | t.Errorf("%q. Secret mismatch, got %v, want %v", tt.name, string(got), string(tt.want)) 120 | } 121 | } 122 | } 123 | 124 | // TestAESCTREncryptorEncrypt ensures the EncryptedData result has the correct 125 | // HMAC, and the correct Type. Actual encrypted cipher-text is checked by the 126 | // integration test. 127 | func TestAESCTREncryptorEncrypt(t *testing.T) { 128 | tests := []struct { 129 | // Test description. 130 | name string 131 | // Parameters. 132 | aesKey []byte 133 | hmacKey []byte 134 | secret []byte 135 | // Expected results. 136 | wantErr bool 137 | }{ 138 | { 139 | "Example", 140 | []byte("iamakey!iamakey!"), 141 | []byte("hmacKey"), 142 | []byte("I am a super secret secret"), 143 | false, 144 | }, 145 | } 146 | for _, tt := range tests { 147 | e, err := NewAES(tt.aesKey, tt.hmacKey) 148 | if err != nil { 149 | t.Errorf("%q. NewAES() error = %v", tt.name, err) 150 | continue 151 | } 152 | 153 | got, err := e.Encrypt(tt.secret) 154 | if (err != nil) != tt.wantErr { 155 | t.Errorf("%q. AESCTREncryptor.Encrypt() error = %v, wantErr %v", tt.name, err, tt.wantErr) 156 | continue 157 | } 158 | 159 | // Check type 160 | if got.Type != AESCTR { 161 | t.Errorf("%q. AESCTREncryptor.Encrypt() result type = %v, want %v", tt.name, got.Type, AESCTR) 162 | } 163 | 164 | // Check HMAC 165 | mac := hmac.New(sha256.New, tt.hmacKey) 166 | mac.Write(got.Ciphertext) 167 | if hmac := mac.Sum(nil); !bytes.Equal(hmac, got.HMAC) { 168 | t.Errorf("%q. AESCTREncryptor.Encrypt() HMAC = %v, want %v", tt.name, got.HMAC, hmac) 169 | } 170 | } 171 | } 172 | 173 | // TestAESCTREncryptorDecrypt ensures an error is returned if either the HMAC is 174 | // invalid, or Decode() is passed a EncryptedData struct of the wrong type. We 175 | // also check the correct secrets are returned from sample cipher-text. 176 | func TestAESCTREncryptorDecrypt(t *testing.T) { 177 | tests := []struct { 178 | // Test description. 179 | name string 180 | // Parameters 181 | data *EncryptedData 182 | // Expected results. 183 | want []byte 184 | wantDecryptErr error 185 | wantOutputErr bool 186 | }{ 187 | { 188 | "Known good", 189 | &EncryptedData{ 190 | Ciphertext: []byte{ 191 | 0x50, 0xc7, 0x16, 0xf8, 0xe8, 0x26, 0x4a, 0xe1, 0xed, 0x1f, 0xe7, 0x82, 0xc2, 0x6f, 192 | 0x41, 0xa3, 0x63, 0x17, 0x18, 0xd9, 0x04, 0x92, 0xbe, 0x68, 0x4d, 0xb3, 0x59, 0xbf, 193 | 0x59, 0x9d, 0xef, 0x3b, 0x92, 0x99, 0x12, 0x3f, 0xc6, 0x59, 0xd9, 0x81, 0xad, 0x78, 194 | }, 195 | HMAC: []byte{ 196 | 0xe8, 0xda, 0xfc, 0x58, 0x5a, 0x84, 0x27, 0x97, 0x13, 0x39, 0x04, 0x7c, 0x85, 0x8e, 0x10, 0xc4, 197 | 0x88, 0x4d, 0x2e, 0xfe, 0x90, 0x5f, 0xc1, 0x8d, 0x93, 0xf5, 0xe5, 0xb4, 0x8a, 0xc5, 0xd6, 0xca, 198 | }, 199 | Type: AESCTR, 200 | }, 201 | []byte("I am a super secret secret"), 202 | nil, 203 | false, 204 | }, 205 | { 206 | "Wrong Encryptor type", 207 | &EncryptedData{ 208 | Ciphertext: []byte{ 209 | 0x50, 0xc7, 0x16, 0xf8, 0xe8, 0x26, 0x4a, 0xe1, 0xed, 0x1f, 0xe7, 0x82, 0xc2, 0x6f, 210 | 0x41, 0xa3, 0x63, 0x17, 0x18, 0xd9, 0x04, 0x92, 0xbe, 0x68, 0x4d, 0xb3, 0x59, 0xbf, 211 | 0x59, 0x9d, 0xef, 0x3b, 0x92, 0x99, 0x12, 0x3f, 0xc6, 0x59, 0xd9, 0x81, 0xad, 0x78, 212 | }, 213 | HMAC: []byte{ 214 | 0xe8, 0xda, 0xfc, 0x58, 0x5a, 0x84, 0x27, 0x97, 0x13, 0x39, 0x04, 0x7c, 0x85, 0x8e, 0x10, 0xc4, 215 | 0x88, 0x4d, 0x2e, 0xfe, 0x90, 0x5f, 0xc1, 0x8d, 0x93, 0xf5, 0xe5, 0xb4, 0x8a, 0xc5, 0xd6, 0xca, 216 | }, 217 | Type: Nop, 218 | }, 219 | []byte{}, 220 | ErrWrongType, 221 | false, 222 | }, 223 | { 224 | "Cipher-text too short (no IV)", 225 | &EncryptedData{ 226 | Ciphertext: []byte{0x42}, 227 | HMAC: []byte{ 228 | 0xe8, 0xda, 0xfc, 0x58, 0x5a, 0x84, 0x27, 0x97, 0x13, 0x39, 0x04, 0x7c, 0x85, 0x8e, 0x10, 0xc4, 229 | 0x88, 0x4d, 0x2e, 0xfe, 0x90, 0x5f, 0xc1, 0x8d, 0x93, 0xf5, 0xe5, 0xb4, 0x8a, 0xc5, 0xd6, 0xca, 230 | }, 231 | Type: AESCTR, 232 | }, 233 | []byte{}, 234 | ErrInvalidHmac, 235 | false, 236 | }, 237 | { 238 | "Bad HMAC", 239 | &EncryptedData{ 240 | Ciphertext: []byte{ 241 | 0x50, 0xc7, 0x16, 0xf8, 0xe8, 0x26, 0x4a, 0xe1, 0xed, 0x1f, 0xe7, 0x82, 0xc2, 0x6f, 242 | 0x41, 0xa3, 0x63, 0x17, 0x18, 0xd9, 0x04, 0x92, 0xbe, 0x68, 0x4d, 0xb3, 0x59, 0xbf, 243 | 0x59, 0x9d, 0xef, 0x3b, 0x92, 0x99, 0x12, 0x3f, 0xc6, 0x59, 0xd9, 0x81, 0xad, 0x78, 244 | }, 245 | HMAC: []byte{ 246 | 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 247 | 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 248 | }, 249 | Type: AESCTR, 250 | }, 251 | []byte{}, 252 | ErrInvalidHmac, 253 | false, 254 | }, 255 | { 256 | "Wrong plain-text", 257 | &EncryptedData{ 258 | Ciphertext: []byte{ 259 | 0x50, 0xc7, 0x16, 0xf8, 0xe8, 0x26, 0x4a, 0xe1, 0xed, 0x1f, 0xe7, 0x82, 0xc2, 0x6f, 260 | 0x41, 0xa3, 0x63, 0x17, 0x18, 0xd9, 0x04, 0x92, 0xbe, 0x68, 0x4d, 0xb3, 0x59, 0xbf, 261 | 0x59, 0x9d, 0xef, 0x3b, 0x92, 0x99, 0x12, 0x3f, 0xc6, 0x59, 0xd9, 0x81, 0xad, 0x78, 262 | }, 263 | HMAC: []byte{ 264 | 0xe8, 0xda, 0xfc, 0x58, 0x5a, 0x84, 0x27, 0x97, 0x13, 0x39, 0x04, 0x7c, 0x85, 0x8e, 0x10, 0xc4, 265 | 0x88, 0x4d, 0x2e, 0xfe, 0x90, 0x5f, 0xc1, 0x8d, 0x93, 0xf5, 0xe5, 0xb4, 0x8a, 0xc5, 0xd6, 0xca, 266 | }, 267 | Type: AESCTR, 268 | }, 269 | []byte("wrong plain-text"), 270 | nil, 271 | true, 272 | }, 273 | } 274 | for _, tt := range tests { 275 | e, err := NewAES([]byte("iamakey!iamakey!"), []byte("hmacKey")) 276 | if err != nil { 277 | t.Errorf("%q. NewAES() error = %v", tt.name, err) 278 | continue 279 | } 280 | 281 | got, err := e.Decrypt(tt.data) 282 | if err != tt.wantDecryptErr { 283 | t.Errorf("%q. AESCTREncryptor.Decrypt() error = %v, wantDecryptErr %v", tt.name, err, tt.wantDecryptErr) 284 | continue 285 | } 286 | 287 | if tt.wantOutputErr == bytes.Equal(tt.want, got) { 288 | t.Errorf("%q. AESCTREncryptor.Decrypt() got = %v, want %v", tt.name, got, tt.want) 289 | continue 290 | } 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /encryptor/encrypted_data.go: -------------------------------------------------------------------------------- 1 | package encryptor 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | ) 7 | 8 | // EncryptedData holds the result of a call to Encrypt(), where Ciphertext is 9 | // the encrypted input, HMAC is the Encryptor-designated hash (typically SHA512) 10 | // of Ciphertext, and Context provides Encryptor-specific information for 11 | // decryption. 12 | // 13 | // Context should not hold any secret information, as the entire EncryptedData 14 | // struct is stored in plain-text by the storage back-end. 15 | type EncryptedData struct { 16 | Ciphertext []byte 17 | HMAC []byte 18 | Type uint8 19 | Context map[string]interface{} 20 | } 21 | 22 | func init() { 23 | gob.Register(kdfParameters{}) 24 | } 25 | 26 | // MarshalBinary returns the EncryptedData struct encoded into a slice of bytes 27 | // using Gob. 28 | func (e EncryptedData) MarshalBinary() ([]byte, error) { 29 | buf := bytes.Buffer{} 30 | enc := gob.NewEncoder(&buf) 31 | 32 | if err := enc.Encode(e.Ciphertext); err != nil { 33 | return nil, err 34 | } 35 | 36 | if err := enc.Encode(e.HMAC); err != nil { 37 | return nil, err 38 | } 39 | 40 | if err := enc.Encode(e.Type); err != nil { 41 | return nil, err 42 | } 43 | 44 | if err := enc.Encode(e.Context); err != nil { 45 | return nil, err 46 | } 47 | 48 | return buf.Bytes(), nil 49 | } 50 | 51 | // UnmarshalBinary returns an EncryptedData struct decoded from a slice of bytes 52 | // using Gob. 53 | func (e *EncryptedData) UnmarshalBinary(data []byte) error { 54 | dec := gob.NewDecoder(bytes.NewReader(data)) 55 | 56 | if err := dec.Decode(&e.Ciphertext); err != nil { 57 | return err 58 | } 59 | 60 | if err := dec.Decode(&e.HMAC); err != nil { 61 | return err 62 | } 63 | 64 | if err := dec.Decode(&e.Type); err != nil { 65 | return err 66 | } 67 | 68 | if err := dec.Decode(&e.Context); err != nil { 69 | return err 70 | } 71 | 72 | return nil 73 | } 74 | -------------------------------------------------------------------------------- /encryptor/encryptor.go: -------------------------------------------------------------------------------- 1 | package encryptor 2 | 3 | // Used to identify different Encryptor types 4 | const ( 5 | Nop uint8 = iota 6 | AESCTR 7 | KMSWrapped 8 | Pbkdf2 9 | AESGCM 10 | ) 11 | 12 | // Encryptor defines the Encrypt method, used to encrypt the given plain-text. 13 | type Encryptor interface { 14 | Encrypt(secret []byte) (*EncryptedData, error) 15 | } 16 | 17 | // Decryptor defines the Decrypt method, used to decrypt the given cipher-text. 18 | type Decryptor interface { 19 | Decrypt(data *EncryptedData) ([]byte, error) 20 | } 21 | 22 | // EncryptDecryptor defines the methods used by our encryptor structs. 23 | type EncryptDecryptor interface { 24 | Encryptor 25 | Decryptor 26 | } 27 | 28 | // EncryptionProvider implementers should return an initalised Encryptor where 29 | // key is the key material for initalisation. 30 | type EncryptionProvider func(key []byte) (EncryptDecryptor, error) 31 | -------------------------------------------------------------------------------- /encryptor/errors.go: -------------------------------------------------------------------------------- 1 | package encryptor 2 | 3 | import "errors" 4 | 5 | var ( 6 | // ErrWrongType indicates a EncryptedData struct was created with a 7 | // different Encryptor than what is being used to decrypt. 8 | ErrWrongType = errors.New("encryptor: wrong encryptor type") 9 | 10 | // ErrInvalidHmac indicates message authentication has failed. 11 | ErrInvalidHmac = errors.New("encryptor: invalid HMAC") 12 | 13 | // ErrKeyTooShort indicates the encryption key provided is too short to be 14 | // useful. 15 | ErrKeyTooShort = errors.New("encryptor: key provided is too short") 16 | 17 | // ErrHmacKeyTooShort indicates the HMAC key is too short to be useful. 18 | ErrHmacKeyTooShort = errors.New("encryptor: HMAC key is required") 19 | 20 | //ErrInvalidCiphertext indicates the ciphertext cannot be decrypted. 21 | ErrInvalidCiphertext = errors.New("encryptor: invalid ciphertext") 22 | 23 | // ErrMissingContext indicates required contextual data is missing. 24 | ErrMissingContext = errors.New("encryptor: missing required context data") 25 | ) 26 | -------------------------------------------------------------------------------- /encryptor/gcm.go: -------------------------------------------------------------------------------- 1 | package encryptor 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "crypto/rand" 7 | "io" 8 | ) 9 | 10 | // AESGCMEncryptor provides AES encryption of secrets using GCM (Galois Counter 11 | // Mode) to ensure data integrity. 12 | type AESGCMEncryptor struct { 13 | gcm cipher.AEAD 14 | } 15 | 16 | // NewAESGCM returns an initalised Encryptor using AES with GCM (Galois Counter 17 | // Mode) to ensure data integrity. 18 | func NewAESGCM(aesKey []byte) (*AESGCMEncryptor, error) { 19 | block, err := aes.NewCipher(aesKey) 20 | if err != nil { 21 | return nil, ErrKeyTooShort 22 | } 23 | 24 | gcm, err := cipher.NewGCM(block) 25 | if err != nil { 26 | // This will never happen, AES uses 128-bit blocks 27 | return nil, err 28 | } 29 | 30 | return &AESGCMEncryptor{gcm: gcm}, nil 31 | } 32 | 33 | // Encrypt generates a unique nonce for each encryption, and encrypts the 34 | // plain-text secret with the configured AES key. 35 | func (e *AESGCMEncryptor) Encrypt(plaintext []byte) (*EncryptedData, error) { 36 | // Generate a random nonce 37 | nonce := make([]byte, e.gcm.NonceSize()) 38 | if _, err := io.ReadFull(rand.Reader, nonce); err != nil { 39 | // No entropy? You've got bigger problems 40 | return nil, err 41 | } 42 | 43 | // Encrypt, appending the ciphertext to the nonce slice 44 | return &EncryptedData{ 45 | Ciphertext: e.gcm.Seal(nonce, nonce, plaintext, nil), 46 | Type: AESGCM, 47 | }, nil 48 | } 49 | 50 | // Decrypt ensures data was encrypted with AESGCMEncryptor before decrypting the 51 | // cipher-text (which also ensures data integrity) and returning the plain-text. 52 | func (e *AESGCMEncryptor) Decrypt(data *EncryptedData) ([]byte, error) { 53 | // Ensure we're operating on something that AESGCMEncryptor encrypted 54 | if data.Type != AESGCM { 55 | return nil, ErrWrongType 56 | } 57 | 58 | // Ensure our input slice is at least gcm.NonceSize() to avoid an 59 | // out-of-bounds access 60 | if len(data.Ciphertext) < e.gcm.NonceSize() { 61 | return nil, ErrInvalidCiphertext 62 | } 63 | 64 | return e.gcm.Open( 65 | nil, 66 | data.Ciphertext[:e.gcm.NonceSize()], 67 | data.Ciphertext[e.gcm.NonceSize():], 68 | nil, 69 | ) 70 | } 71 | -------------------------------------------------------------------------------- /encryptor/gcm_test.go: -------------------------------------------------------------------------------- 1 | package encryptor 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | // TestNewAESGCM ensures invalid input returns the correct error types. 9 | func TestNewAESGCM(t *testing.T) { 10 | tests := []struct { 11 | // Test description. 12 | name string 13 | // Parameters. 14 | aesKey []byte 15 | // Expected results. 16 | wantErr error 17 | }{ 18 | { 19 | "Correct", 20 | []byte("12345678901234567890123456789012"), 21 | nil, 22 | }, 23 | { 24 | "AES key required", 25 | []byte{}, 26 | ErrKeyTooShort, 27 | }, 28 | { 29 | "Error with wrong AES key length", 30 | []byte("short"), 31 | ErrKeyTooShort, 32 | }, 33 | } 34 | for _, tt := range tests { 35 | _, err := NewAESGCM(tt.aesKey) 36 | 37 | if err != tt.wantErr { 38 | t.Errorf("%q. NewAESGCM() error = %v, wantErr %v", tt.name, err, tt.wantErr) 39 | } 40 | } 41 | } 42 | 43 | // TestAesGCMEncryptorIntegration ensures Encrypt() and Decrypt() work together 44 | // to produce the same plain-text as the original input. 45 | func TestAESGCMEncryptorIntegration(t *testing.T) { 46 | tests := []struct { 47 | // Test description. 48 | name string 49 | // Parameters. 50 | want []byte 51 | }{ 52 | { 53 | "Simple string", 54 | []byte("i am a secret"), 55 | }, 56 | { 57 | "Binary", 58 | []byte{ 59 | 0xb0, 0x75, 0x11, 0x62, 0xa2, 0x3e, 0x5f, 0x2f, 60 | 0xca, 0xa3, 0x00, 0x1d, 0x51, 0x89, 0xc8, 0xe7, 61 | 0xb5, 0x15, 0xb9, 0x5c, 0x9b, 0x3e, 0x26, 0x5f, 62 | 0xb2, 0x6b, 0x97, 0x41, 0x16, 0x2c, 0x47, 0x10, 63 | }, 64 | }, 65 | } 66 | 67 | for _, tt := range tests { 68 | e, err := NewAESGCM([]byte("anAesTestKey1234")) 69 | if err != nil { 70 | t.Errorf("%q. NewAESGCM() = %s", tt.name, err) 71 | continue 72 | } 73 | 74 | encrypted, err := e.Encrypt(tt.want) 75 | if err != nil { 76 | t.Errorf("%q. Encrypt() = %s", tt.name, err) 77 | continue 78 | } 79 | 80 | got, err := e.Decrypt(encrypted) 81 | if err != nil { 82 | t.Errorf("%q. Decrypt() = %s", tt.name, err) 83 | continue 84 | } 85 | 86 | if !bytes.Equal(got, tt.want) { 87 | t.Errorf("%q. Secret mismatch, got %v, want %v", tt.name, string(got), string(tt.want)) 88 | } 89 | } 90 | } 91 | 92 | // TestAESGCMEncrypt ensures the EncryptedData returned from Encrypt() has the 93 | // correct attributes set. 94 | func TestAESGCMEncryptorEncrypt(t *testing.T) { 95 | e, err := NewAESGCM([]byte("anAesTestKey1234")) 96 | if err != nil { 97 | t.Fatalf("Encrypt. NewAESGCM() = %s", err) 98 | } 99 | 100 | encrypted, err := e.Encrypt([]byte("i am a secret")) 101 | if err != nil { 102 | t.Fatalf("Encrypt. Encrypt() = %s", err) 103 | } 104 | 105 | if encrypted.Type != AESGCM { 106 | t.Fatalf("Type. Encrypt() got = %v, want %v", encrypted.Type, AESGCM) 107 | } 108 | } 109 | 110 | // TestAESGCMEncryptorDecrypt ensures an error is returned if either the HMAC is 111 | // invalid, or Decode() is passed a EncryptedData struct of the wrong type. We 112 | // also check the correct secrets are returned from sample cipher-text. 113 | func TestAESGCMEncryptorDecrypt(t *testing.T) { 114 | tests := []struct { 115 | // Test description. 116 | name string 117 | // Parameters 118 | data *EncryptedData 119 | // Expected results. 120 | want []byte 121 | wantDecryptErr error 122 | wantOutputErr bool 123 | }{ 124 | { 125 | "Known good", 126 | &EncryptedData{ 127 | Ciphertext: []byte{ 128 | 0xef, 0x49, 0x98, 0x28, 0xd1, 0x45, 0x99, 0xa9, 0xf6, 0x79, 0x06, 0x3d, 0x76, 0xec, 129 | 0x0f, 0xc7, 0xfb, 0x2b, 0x2b, 0xe9, 0xd3, 0xcf, 0x6e, 0xa2, 0xed, 0x7f, 0x10, 0x70, 130 | 0x4f, 0x01, 0xce, 0x9f, 0x18, 0x7a, 0x67, 0xc7, 0xb4, 0x6e, 0x91, 0x45, 0x60, 131 | }, 132 | Type: AESGCM, 133 | }, 134 | []byte("it's a secret"), 135 | nil, 136 | false, 137 | }, 138 | { 139 | "Wrong Encryptor type", 140 | &EncryptedData{ 141 | Ciphertext: []byte{ 142 | 0xef, 0x49, 0x98, 0x28, 0xd1, 0x45, 0x99, 0xa9, 0xf6, 0x79, 0x06, 0x3d, 0x76, 0xec, 143 | 0x0f, 0xc7, 0xfb, 0x2b, 0x2b, 0xe9, 0xd3, 0xcf, 0x6e, 0xa2, 0xed, 0x7f, 0x10, 0x70, 144 | 0x4f, 0x01, 0xce, 0x9f, 0x18, 0x7a, 0x67, 0xc7, 0xb4, 0x6e, 0x91, 0x45, 0x60, 145 | }, 146 | Type: Nop, 147 | }, 148 | []byte{}, 149 | ErrWrongType, 150 | false, 151 | }, 152 | { 153 | "Cipher-text too short (no nonce)", 154 | &EncryptedData{ 155 | Ciphertext: []byte{0x42}, 156 | Type: AESGCM, 157 | }, 158 | []byte{}, 159 | ErrInvalidCiphertext, 160 | false, 161 | }, 162 | { 163 | "Wrong plain-text", 164 | &EncryptedData{ 165 | Ciphertext: []byte{ 166 | 0xef, 0x49, 0x98, 0x28, 0xd1, 0x45, 0x99, 0xa9, 0xf6, 0x79, 0x06, 0x3d, 0x76, 0xec, 167 | 0x0f, 0xc7, 0xfb, 0x2b, 0x2b, 0xe9, 0xd3, 0xcf, 0x6e, 0xa2, 0xed, 0x7f, 0x10, 0x70, 168 | 0x4f, 0x01, 0xce, 0x9f, 0x18, 0x7a, 0x67, 0xc7, 0xb4, 0x6e, 0x91, 0x45, 0x60, 169 | }, 170 | Type: AESGCM, 171 | }, 172 | []byte("wrong plain-text"), 173 | nil, 174 | true, 175 | }, 176 | } 177 | for _, tt := range tests { 178 | e, err := NewAESGCM([]byte("anAesTestKey1234")) 179 | if err != nil { 180 | t.Errorf("%q. NewAESGCM() error = %v", tt.name, err) 181 | continue 182 | } 183 | 184 | got, err := e.Decrypt(tt.data) 185 | if err != tt.wantDecryptErr { 186 | t.Errorf("%q. AESGCMEncryptor.Decrypt() error = %v, wantDecryptErr %v", tt.name, err, tt.wantDecryptErr) 187 | continue 188 | } 189 | 190 | if tt.wantOutputErr == bytes.Equal(tt.want, got) { 191 | t.Errorf("%q. AESGCMEncryptor.Decrypt() got = %v, want %v", tt.name, got, tt.want) 192 | continue 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /encryptor/kdf.go: -------------------------------------------------------------------------------- 1 | package encryptor 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/sha512" 6 | 7 | "golang.org/x/crypto/pbkdf2" 8 | ) 9 | 10 | const kdfKeySize = 64 11 | 12 | // KDF implements PBKDF2, deriving a key using SHA-512 and passing it to 13 | // Provider. 14 | // 15 | // By default Provider is AES-512. 16 | type KDF struct { 17 | Provider EncryptionProvider 18 | SaltSize int 19 | Iterations int 20 | SourceKey []byte 21 | } 22 | 23 | type kdfParameters struct { 24 | Salt []byte 25 | OrigType uint8 26 | Iterations int 27 | } 28 | 29 | // NewKDF by default returns a AESCTR struct that has been wrapped with KDF, 30 | // enabling PBKDF2 support. 31 | func NewKDF(sourceKey []byte) (*KDF, error) { 32 | if len(sourceKey) < 1 { 33 | return nil, ErrKeyTooShort 34 | } 35 | 36 | builder := func(key []byte) (EncryptDecryptor, error) { 37 | return NewAES(key[:32], key[32:]) 38 | } 39 | 40 | return &KDF{ 41 | Provider: builder, 42 | SaltSize: 16, 43 | Iterations: 4096, 44 | SourceKey: sourceKey, 45 | }, nil 46 | } 47 | 48 | // Encrypt uses the Encryptor returned by Provider, supplying it with key 49 | // material derived from SourceKey using SHA-512. 50 | func (e KDF) Encrypt(secret []byte) (*EncryptedData, error) { 51 | // Get a random salt 52 | salt := make([]byte, e.SaltSize) 53 | _, err := rand.Read(salt) 54 | if err != nil { 55 | // No entropy? You've got bigger problems 56 | return nil, err 57 | } 58 | 59 | // Derive a key 60 | key := pbkdf2.Key(e.SourceKey, salt, e.Iterations, kdfKeySize, sha512.New) 61 | 62 | // Get a new encryptor using the provided key 63 | enc, err := e.Provider(key) 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | // Let our encryption provider do it's thing 69 | data, err := enc.Encrypt(secret) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | if data.Context == nil { 75 | data.Context = map[string]interface{}{} 76 | } 77 | 78 | p := kdfParameters{ 79 | Salt: salt, 80 | Iterations: e.Iterations, 81 | OrigType: data.Type, 82 | } 83 | 84 | // Store the original Encryptor type in the context 85 | data.Context["kdf"] = p 86 | data.Type = Pbkdf2 87 | 88 | return data, nil 89 | } 90 | 91 | // Decrypt uses values stored in Context to derive the key material from 92 | // SourceKey, and passes it to the Encryptor provided by Provider. 93 | func (e KDF) Decrypt(data *EncryptedData) ([]byte, error) { 94 | // Ensure this data used KDF 95 | if data.Type != Pbkdf2 { 96 | return []byte{}, ErrWrongType 97 | } 98 | 99 | // Extract the KDF context 100 | ctxInt, ok := data.Context["kdf"] 101 | if !ok { 102 | return []byte{}, ErrMissingContext 103 | } 104 | 105 | ctx, ok := ctxInt.(kdfParameters) 106 | if !ok { 107 | return []byte{}, ErrMissingContext 108 | } 109 | 110 | // Generate the key 111 | key := pbkdf2.Key(e.SourceKey, ctx.Salt, ctx.Iterations, kdfKeySize, sha512.New) 112 | 113 | // Give it to the decryption provider 114 | dec, err := e.Provider(key) 115 | if err != nil { 116 | return []byte{}, err 117 | } 118 | 119 | // Get a mutable copy of data so we don't alter the original and replace the type 120 | mutable := *data 121 | mutable.Type = ctx.OrigType 122 | 123 | return dec.Decrypt(&mutable) 124 | } 125 | -------------------------------------------------------------------------------- /encryptor/kdf_test.go: -------------------------------------------------------------------------------- 1 | package encryptor 2 | 3 | import ( 4 | "bytes" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | // TestAESKDFEncryptorIntegration ensures a key is generated, secret encrypted 10 | // with AES, and decrypted to the same plaintext 11 | func TestAESKDFEncryptorIntegration(t *testing.T) { 12 | tests := []struct { 13 | // Test description. 14 | name string 15 | // Parameters. 16 | want []byte 17 | skey []byte 18 | }{ 19 | { 20 | "Simple string", 21 | []byte("i am a secret"), 22 | []byte("smallkey!"), 23 | }, 24 | { 25 | "Binary", 26 | []byte{ 27 | 0xb0, 0x75, 0x11, 0x62, 0xa2, 0x3e, 0x5f, 0x2f, 28 | 0xca, 0xa3, 0x00, 0x1d, 0x51, 0x89, 0xc8, 0xe7, 29 | 0xb5, 0x15, 0xb9, 0x5c, 0x9b, 0x3e, 0x26, 0x5f, 30 | 0xb2, 0x6b, 0x97, 0x41, 0x16, 0x2c, 0x47, 0x10, 31 | }, 32 | []byte{0x42}, 33 | }, 34 | } 35 | 36 | for _, tt := range tests { 37 | e, err := NewKDF(tt.skey) 38 | if err != nil { 39 | t.Errorf("%q. NewKDF() = %s", tt.name, err) 40 | continue 41 | } 42 | 43 | encrypted, err := e.Encrypt(tt.want) 44 | if err != nil { 45 | t.Errorf("%q. Encrypt() = %s", tt.name, err) 46 | continue 47 | } 48 | 49 | got, err := e.Decrypt(encrypted) 50 | if err != nil { 51 | t.Errorf("%q. Decrypt() = %s", tt.name, err) 52 | continue 53 | } 54 | 55 | if !bytes.Equal(got, tt.want) { 56 | t.Errorf("%q. Secret mismatch, got %v, want %v", tt.name, got, tt.want) 57 | } 58 | } 59 | } 60 | 61 | // TestAESGCMKDFEncryptorIntegration ensures a key is generated, secret 62 | // encrypted with AESGCM, and decrypted to the same plaintext. 63 | func TestAESGCMKDFEncryptorIntegration(t *testing.T) { 64 | tests := []struct { 65 | // Test description. 66 | name string 67 | // Parameters. 68 | want []byte 69 | skey []byte 70 | }{ 71 | { 72 | "Simple string", 73 | []byte("i am a secret"), 74 | []byte("smallkey!"), 75 | }, 76 | { 77 | "Binary", 78 | []byte{ 79 | 0xb0, 0x75, 0x11, 0x62, 0xa2, 0x3e, 0x5f, 0x2f, 80 | 0xca, 0xa3, 0x00, 0x1d, 0x51, 0x89, 0xc8, 0xe7, 81 | 0xb5, 0x15, 0xb9, 0x5c, 0x9b, 0x3e, 0x26, 0x5f, 82 | 0xb2, 0x6b, 0x97, 0x41, 0x16, 0x2c, 0x47, 0x10, 83 | }, 84 | []byte{0x42}, 85 | }, 86 | } 87 | 88 | for _, tt := range tests { 89 | e, err := NewKDF(tt.skey) 90 | e.Provider = func(key []byte) (EncryptDecryptor, error) { 91 | return NewAESGCM(key[:32]) 92 | } 93 | 94 | if err != nil { 95 | t.Errorf("%q. NewKDF() = %s", tt.name, err) 96 | continue 97 | } 98 | 99 | encrypted, err := e.Encrypt(tt.want) 100 | if err != nil { 101 | t.Errorf("%q. Encrypt() = %s", tt.name, err) 102 | continue 103 | } 104 | 105 | got, err := e.Decrypt(encrypted) 106 | if err != nil { 107 | t.Errorf("%q. Decrypt() = %s", tt.name, err) 108 | continue 109 | } 110 | 111 | if !bytes.Equal(got, tt.want) { 112 | t.Errorf("%q. Secret mismatch, got %v, want %v", tt.name, got, tt.want) 113 | } 114 | } 115 | } 116 | 117 | // Ensure errors are passed up to the caller 118 | func TestKDFEncrypt(t *testing.T) { 119 | tests := []struct { 120 | // Test description. 121 | name string 122 | // Receiver fields. 123 | rProvider EncryptionProvider 124 | rSourceKey []byte 125 | // Parameters. 126 | secret []byte 127 | // Expected results. 128 | wantErr error 129 | }{ 130 | { 131 | "Known good", 132 | func(key []byte) (EncryptDecryptor, error) { 133 | return NopEncryptor{}, nil 134 | }, 135 | []byte("key"), 136 | []byte("secret"), 137 | nil, 138 | }, 139 | { 140 | "Encryptor initalise errors passed up", 141 | func(key []byte) (EncryptDecryptor, error) { 142 | return nil, errMarker 143 | }, 144 | []byte("key"), 145 | []byte("secret"), 146 | errMarker, 147 | }, 148 | { 149 | "Encryptor Encrypt() errors passed up", 150 | func(key []byte) (EncryptDecryptor, error) { 151 | return &errEncryptor{errMarker}, nil 152 | }, 153 | []byte("key"), 154 | []byte("secret"), 155 | errMarker, 156 | }, 157 | } 158 | for _, tt := range tests { 159 | e := KDF{ 160 | Provider: tt.rProvider, 161 | SaltSize: 16, 162 | Iterations: 32, // small for testing 163 | SourceKey: tt.rSourceKey, 164 | } 165 | got, err := e.Encrypt(tt.secret) 166 | if err != tt.wantErr { 167 | t.Errorf("%q. KDF.Encrypt() error = %v, wantErr %v", tt.name, err, tt.wantErr) 168 | continue 169 | } 170 | if err != nil { 171 | continue 172 | } 173 | 174 | if !bytes.Equal(got.Ciphertext, tt.secret) { 175 | t.Errorf("%q. KDF.Encrypt() = %v, want %v", tt.name, got.Ciphertext, tt.secret) 176 | } 177 | 178 | if got.Type != Pbkdf2 { 179 | t.Errorf("%q. KDF.Encrypt() type = %v, want %v", tt.name, got.Type, Pbkdf2) 180 | } 181 | 182 | if got.Context == nil { 183 | t.Errorf("%q. KDF.Encrypt() context map = %v", tt.name, got.Context) 184 | continue 185 | } 186 | 187 | ctxInt, ok := got.Context["kdf"] 188 | if !ok { 189 | t.Errorf("%q. KDF.Encrypt() context = %v", tt.name, nil) 190 | continue 191 | } 192 | 193 | ctx, ok := ctxInt.(kdfParameters) 194 | if !ok { 195 | t.Errorf("%q. KDF.Encrypt() context = %T", tt.name, ctxInt) 196 | continue 197 | } 198 | 199 | if ctx.OrigType != Nop { 200 | t.Errorf("%q. KDF.Encrypt() OrigType = %v, want %v", tt.name, ctx.OrigType, Nop) 201 | } 202 | 203 | if ctx.Iterations != 32 { 204 | t.Errorf("%q. KDF.Encrypt() Iterations = %v, want %v", tt.name, ctx.Iterations, 32) 205 | } 206 | 207 | if len(ctx.Salt) != 16 { 208 | t.Errorf("%q. KDF.Encrypt() len(Salt) = %v, want %v", tt.name, len(ctx.Salt), 16) 209 | } 210 | } 211 | } 212 | 213 | func TestKDFDecrypt(t *testing.T) { 214 | tests := []struct { 215 | // Test description. 216 | name string 217 | // Receiver fields. 218 | rProvider EncryptionProvider 219 | rSourceKey []byte 220 | // Parameters. 221 | data *EncryptedData 222 | // Expected results. 223 | want []byte 224 | wantErr error 225 | }{ 226 | { 227 | "Known good - NOP", 228 | func(key []byte) (EncryptDecryptor, error) { 229 | return NopEncryptor{}, nil 230 | }, 231 | []byte("key"), 232 | &EncryptedData{ 233 | Ciphertext: []byte("secret"), 234 | HMAC: []byte("--ignored--"), 235 | Type: Pbkdf2, 236 | Context: map[string]interface{}{ 237 | "kdf": kdfParameters{ 238 | Salt: []byte{ 239 | 0xbf, 0x19, 0x6d, 0x5e, 0xc6, 0xa0, 0x70, 0x5b, 240 | 0x45, 0xff, 0x36, 0x04, 0xf7, 0xa3, 0x3f, 0xd5, 241 | }, 242 | Iterations: 32, 243 | OrigType: Nop, 244 | }, 245 | }, 246 | }, 247 | []byte("secret"), 248 | nil, 249 | }, 250 | { 251 | "Known good - AES", 252 | func(key []byte) (EncryptDecryptor, error) { 253 | return NewAES(key[:32], key[32:]) 254 | }, 255 | []byte("key"), 256 | &EncryptedData{ 257 | Ciphertext: []byte{ 258 | 0x69, 0x6b, 0xb7, 0x4e, 0x41, 0x76, 0x6a, 0x9c, 0x74, 0x54, 0xf4, 259 | 0x2a, 0x89, 0x86, 0x65, 0x91, 0x64, 0x89, 0x5b, 0xb0, 0x16, 0xda, 260 | }, 261 | HMAC: []byte{ 262 | 0x53, 0x3a, 0xd6, 0x8d, 0x87, 0xb2, 0x98, 0xc4, 0x11, 0x2d, 0xde, 263 | 0x39, 0xe5, 0x00, 0xfa, 0xa0, 0x28, 0x91, 0xd4, 0xb0, 0x34, 0xcc, 264 | 0x2d, 0xc6, 0x05, 0xbd, 0xf5, 0x8a, 0xb2, 0x72, 0xb5, 0x55, 265 | }, 266 | Type: Pbkdf2, 267 | Context: map[string]interface{}{ 268 | "kdf": kdfParameters{ 269 | Salt: []byte{ 270 | 0xbf, 0x19, 0x6d, 0x5e, 0xc6, 0xa0, 0x70, 0x5b, 271 | 0x45, 0xff, 0x36, 0x04, 0xf7, 0xa3, 0x3f, 0xd5, 272 | }, 273 | Iterations: 32, 274 | OrigType: AESCTR, 275 | }, 276 | }, 277 | }, 278 | []byte("secret"), 279 | nil, 280 | }, 281 | { 282 | "Encryptor initalise errors passed up", 283 | func(key []byte) (EncryptDecryptor, error) { 284 | return nil, errMarker 285 | }, 286 | []byte("key"), 287 | &EncryptedData{ 288 | Ciphertext: []byte("secret"), 289 | HMAC: []byte("--ignored--"), 290 | Type: Pbkdf2, 291 | Context: map[string]interface{}{ 292 | "kdf": kdfParameters{ 293 | Salt: []byte{ 294 | 0xbf, 0x19, 0x6d, 0x5e, 0xc6, 0xa0, 0x70, 0x5b, 295 | 0x45, 0xff, 0x36, 0x04, 0xf7, 0xa3, 0x3f, 0xd5, 296 | }, 297 | Iterations: 32, 298 | OrigType: Nop, 299 | }, 300 | }, 301 | }, 302 | []byte{}, 303 | errMarker, 304 | }, 305 | { 306 | "Encryptor Decrypt() errors passed up", 307 | func(key []byte) (EncryptDecryptor, error) { 308 | return &errEncryptor{errMarker}, nil 309 | }, 310 | []byte("key"), 311 | &EncryptedData{ 312 | Ciphertext: []byte("secret"), 313 | HMAC: []byte("--ignored--"), 314 | Type: Pbkdf2, 315 | Context: map[string]interface{}{ 316 | "kdf": kdfParameters{ 317 | Salt: []byte{ 318 | 0xbf, 0x19, 0x6d, 0x5e, 0xc6, 0xa0, 0x70, 0x5b, 319 | 0x45, 0xff, 0x36, 0x04, 0xf7, 0xa3, 0x3f, 0xd5, 320 | }, 321 | Iterations: 32, 322 | OrigType: Nop, 323 | }, 324 | }, 325 | }, 326 | []byte{}, 327 | errMarker, 328 | }, 329 | { 330 | "Wrong type", 331 | func(key []byte) (EncryptDecryptor, error) { 332 | return NopEncryptor{}, nil 333 | }, 334 | []byte("key"), 335 | &EncryptedData{ 336 | Ciphertext: []byte("secret"), 337 | HMAC: []byte("--ignored--"), 338 | Type: Nop, 339 | Context: map[string]interface{}{ 340 | "kdf": kdfParameters{ 341 | Salt: []byte{ 342 | 0xbf, 0x19, 0x6d, 0x5e, 0xc6, 0xa0, 0x70, 0x5b, 343 | 0x45, 0xff, 0x36, 0x04, 0xf7, 0xa3, 0x3f, 0xd5, 344 | }, 345 | Iterations: 32, 346 | OrigType: Nop, 347 | }, 348 | }, 349 | }, 350 | []byte("secret"), 351 | ErrWrongType, 352 | }, 353 | } 354 | for _, tt := range tests { 355 | e := KDF{ 356 | Provider: tt.rProvider, 357 | SaltSize: 16, 358 | Iterations: 32, // small for testing 359 | SourceKey: tt.rSourceKey, 360 | } 361 | got, err := e.Decrypt(tt.data) 362 | if err != tt.wantErr { 363 | t.Errorf("%q. KDF.Decrypt() error = %v, wantErr %v", tt.name, err, tt.wantErr) 364 | continue 365 | } 366 | if err != nil { 367 | continue 368 | } 369 | 370 | if !reflect.DeepEqual(got, tt.want) { 371 | t.Errorf("%q. KDF.Decrypt() = %v, want %v", tt.name, got, tt.want) 372 | } 373 | } 374 | } 375 | -------------------------------------------------------------------------------- /encryptor/kms.go: -------------------------------------------------------------------------------- 1 | package encryptor 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws" 5 | "github.com/aws/aws-sdk-go/aws/session" 6 | "github.com/aws/aws-sdk-go/service/kms" 7 | ) 8 | 9 | // KMS is used to wrap the output of any other Encryptor using Amazon KMS, by 10 | // default using AES-256. 11 | type KMS struct { 12 | svc kmsInterface 13 | keyID string 14 | KeySize int64 15 | Provider EncryptionProvider 16 | } 17 | 18 | type kmsInterface interface { 19 | GenerateDataKey(input *kms.GenerateDataKeyInput) (*kms.GenerateDataKeyOutput, error) 20 | Decrypt(input *kms.DecryptInput) (*kms.DecryptOutput, error) 21 | } 22 | 23 | // NewKMS returns an initialised Encryptor using Amazon KMS to wrap the 24 | // underlying Encryptor's keys used to encrypt secrets. 25 | // 26 | // By default, KMS uses AESCTREncryptor with a 32 byte key (AES-256). 27 | func NewKMS(keyID, region string) *KMS { 28 | 29 | // By default, we use AES-256, which takes a 32 byte key, and we use the 30 | // rest for the HMAC key 31 | 32 | builder := func(key []byte) (EncryptDecryptor, error) { 33 | // Ensure we have at least a 64 byte key to split 34 | if len(key) < 64 { 35 | return nil, ErrKeyTooShort 36 | } 37 | 38 | return NewAES(key[:32], key[32:]) 39 | } 40 | 41 | return &KMS{ 42 | svc: kms.New(session.New(), &aws.Config{Region: aws.String(region)}), 43 | keyID: keyID, 44 | KeySize: 64, 45 | Provider: builder, 46 | } 47 | } 48 | 49 | // Encrypt generates a new encryption key using Amazon KMS, passing it to the 50 | // configured EncryptionProvider as the encryption key to encrypt the secret. 51 | func (e *KMS) Encrypt(secret []byte) (*EncryptedData, error) { 52 | // Ask KMS for a 64 byte encryption key 53 | resp, err := e.svc.GenerateDataKey(&kms.GenerateDataKeyInput{ 54 | KeyId: &e.keyID, 55 | NumberOfBytes: aws.Int64(e.KeySize), 56 | }) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | // Get a new encryptor using the provided key 62 | enc, err := e.Provider(resp.Plaintext) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | // Let our encryption provider do it's thing 68 | data, err := enc.Encrypt(secret) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | // Now data holds an EncryptedData struct, but we need to mutate it before 74 | // returning - change the Type and add our contextual info 75 | 76 | if data.Context == nil { 77 | data.Context = map[string]interface{}{} 78 | } 79 | 80 | // Store the original Encryptor type in the context 81 | data.Context["kms_type"] = data.Type 82 | data.Type = KMSWrapped 83 | 84 | // Store our KMS wrapped key in the context 85 | data.Context["kms_key"] = resp.CiphertextBlob 86 | 87 | return data, nil 88 | } 89 | 90 | // Decrypt decrypts the embedded encyption key using Amazon KMS, and then passes 91 | // the plain-text key to the EncryptionProvider to decrypt the secret. 92 | func (e *KMS) Decrypt(data *EncryptedData) ([]byte, error) { 93 | // Ensure this data was wrapped 94 | if data.Type != KMSWrapped { 95 | return []byte{}, ErrWrongType 96 | } 97 | 98 | // Extract the encrypted key 99 | kmsKeyInt, ok := data.Context["kms_key"] 100 | if !ok { 101 | return []byte{}, ErrMissingContext 102 | } 103 | 104 | kmsKey, ok := kmsKeyInt.([]byte) 105 | if !ok { 106 | return []byte{}, ErrMissingContext 107 | } 108 | 109 | // Extract the orignal type early so we can avoid calling KMS unless we're 110 | // good to go 111 | origTypeInt, ok := data.Context["kms_type"] 112 | if !ok { 113 | return []byte{}, ErrMissingContext 114 | } 115 | 116 | origType, ok := origTypeInt.(uint8) 117 | if !ok { 118 | return []byte{}, ErrMissingContext 119 | } 120 | 121 | // Decrypt the key 122 | resp, err := e.svc.Decrypt(&kms.DecryptInput{CiphertextBlob: kmsKey}) 123 | if err != nil { 124 | return []byte{}, err 125 | } 126 | 127 | // Feed the key back into our Decryptor 128 | dec, err := e.Provider(resp.Plaintext) 129 | if err != nil { 130 | return []byte{}, err 131 | } 132 | 133 | // Get a mutable copy of data so we don't alter the original and replace the type 134 | mutable := *data 135 | mutable.Type = origType 136 | 137 | return dec.Decrypt(&mutable) 138 | } 139 | -------------------------------------------------------------------------------- /encryptor/kms_integration_test.go: -------------------------------------------------------------------------------- 1 | // +build awsintegration 2 | 3 | package encryptor 4 | 5 | import ( 6 | "bytes" 7 | "os" 8 | "testing" 9 | ) 10 | 11 | // TestKMSAWSIntegration performs an encryption and decryption using live KMS 12 | // details, ensuring we get the same output as input. 13 | func TestKMSAWSIntegration(t *testing.T) { 14 | if os.Getenv("AWS_REGION") == "" { 15 | t.Skip("no AWS_REGION environment variable set, skipping KMS integration tests") 16 | } 17 | 18 | if os.Getenv("KMS_KEY_ID") == "" { 19 | t.Skip("no KMS_KEY_ID environment variable set, skipping KMS integration tests") 20 | } 21 | 22 | tests := []struct { 23 | // Test description. 24 | name string 25 | // Parameters. 26 | secret []byte 27 | // Expected results. 28 | wantEncryptErr error 29 | wantDecryptErr error 30 | }{ 31 | { 32 | "Known good", 33 | []byte("secret"), 34 | nil, 35 | nil, 36 | }, 37 | } 38 | 39 | for _, tt := range tests { 40 | e := NewKMS(os.Getenv("KMS_KEY_ID"), os.Getenv("AWS_REGION")) 41 | 42 | encd, err := e.Encrypt(tt.secret) 43 | if err != tt.wantEncryptErr { 44 | t.Errorf("%q. KMS.Encrypt() error = %v, wantEncryptErr %v", tt.name, err, tt.wantEncryptErr) 45 | continue 46 | } 47 | if err != nil { 48 | continue 49 | } 50 | 51 | got, err := e.Decrypt(encd) 52 | if err != tt.wantDecryptErr { 53 | t.Errorf("%q. KMS.Decrypt() error = %v, wantDecryptErr %v", tt.name, err, tt.wantDecryptErr) 54 | continue 55 | } 56 | if err != nil { 57 | continue 58 | } 59 | 60 | if !bytes.Equal(got, tt.secret) { 61 | t.Errorf("%q. KMS.Decrypt() = %v, want %v", tt.name, got, tt.secret) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /encryptor/kms_test.go: -------------------------------------------------------------------------------- 1 | package encryptor 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/aws/aws-sdk-go/aws" 10 | "github.com/aws/aws-sdk-go/service/kms" 11 | ) 12 | 13 | var ( 14 | errGenerateDataKey = errors.New("GenerateDataKey error") 15 | errMarker = errors.New("any error") 16 | ) 17 | 18 | type mockKms struct { 19 | keyID string 20 | err error 21 | } 22 | 23 | func (m *mockKms) GenerateDataKey(input *kms.GenerateDataKeyInput) (*kms.GenerateDataKeyOutput, error) { 24 | if m.err != nil { 25 | return nil, m.err 26 | } 27 | 28 | if m.keyID != *input.KeyId { 29 | return nil, errGenerateDataKey 30 | } 31 | 32 | return &kms.GenerateDataKeyOutput{ 33 | CiphertextBlob: []byte("AAAA"), 34 | KeyId: aws.String("KEY"), 35 | Plaintext: []byte("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY"), 36 | }, nil 37 | } 38 | 39 | func (m *mockKms) Decrypt(input *kms.DecryptInput) (*kms.DecryptOutput, error) { 40 | if m.err != nil { 41 | return nil, m.err 42 | } 43 | 44 | return &kms.DecryptOutput{ 45 | KeyId: aws.String("KEY"), 46 | Plaintext: []byte("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY"), 47 | }, nil 48 | } 49 | 50 | type errEncryptor struct { 51 | err error 52 | } 53 | 54 | func (e *errEncryptor) Encrypt(secret []byte) (*EncryptedData, error) { 55 | return nil, e.err 56 | } 57 | 58 | func (e *errEncryptor) Decrypt(data *EncryptedData) ([]byte, error) { 59 | return nil, e.err 60 | } 61 | 62 | func TestKMSEncrypt(t *testing.T) { 63 | tests := []struct { 64 | // Test description. 65 | name string 66 | // Receiver fields. 67 | rsvc kmsInterface 68 | rkeyID string 69 | rProvider EncryptionProvider 70 | // Parameters. 71 | secret []byte 72 | // Expected results. 73 | want *EncryptedData 74 | wantErr error 75 | }{ 76 | { 77 | "Known good", 78 | &mockKms{keyID: "keyId"}, 79 | "keyId", 80 | // Use NopEncryptor for testing 81 | func(key []byte) (EncryptDecryptor, error) { 82 | return NopEncryptor{}, nil 83 | }, 84 | []byte("secret"), 85 | &EncryptedData{ 86 | Ciphertext: []byte("secret"), 87 | HMAC: []byte("--ignored--"), 88 | Type: KMSWrapped, 89 | Context: map[string]interface{}{ 90 | "kms_type": Nop, 91 | "kms_key": []byte("AAAA"), 92 | }, 93 | }, 94 | nil, 95 | }, 96 | { 97 | "KMS request error", 98 | &mockKms{keyID: "keyId", err: errMarker}, 99 | "keyId", 100 | // Use NopEncryptor for testing 101 | func(key []byte) (EncryptDecryptor, error) { 102 | return NopEncryptor{}, nil 103 | }, 104 | []byte("secret"), 105 | &EncryptedData{}, 106 | errMarker, 107 | }, 108 | { 109 | "Encryptor initalise errors passed up", 110 | &mockKms{keyID: "keyId"}, 111 | "keyId", 112 | // Use NopEncryptor for testing 113 | func(key []byte) (EncryptDecryptor, error) { 114 | return nil, errMarker 115 | }, 116 | []byte("secret"), 117 | &EncryptedData{}, 118 | errMarker, 119 | }, 120 | { 121 | "Encryptor Encrypt() errors passed up", 122 | &mockKms{keyID: "keyId"}, 123 | "keyId", 124 | // Use NopEncryptor for testing 125 | func(key []byte) (EncryptDecryptor, error) { 126 | return &errEncryptor{errMarker}, nil 127 | }, 128 | []byte("secret"), 129 | &EncryptedData{}, 130 | errMarker, 131 | }, 132 | } 133 | 134 | for _, tt := range tests { 135 | e := &KMS{ 136 | svc: tt.rsvc, 137 | keyID: tt.rkeyID, 138 | KeySize: 64, 139 | Provider: tt.rProvider, 140 | } 141 | got, err := e.Encrypt(tt.secret) 142 | 143 | if err != tt.wantErr { 144 | t.Errorf("%q. KMS.Encrypt() error = %v, wantErr %v", tt.name, err, tt.wantErr) 145 | continue 146 | } 147 | 148 | if err != nil { 149 | continue 150 | } 151 | 152 | if !reflect.DeepEqual(got, tt.want) { 153 | t.Errorf("%q. KMS.Encrypt() = %v, want %v", tt.name, got, tt.want) 154 | } 155 | } 156 | } 157 | 158 | func TestKMSDecrypt(t *testing.T) { 159 | tests := []struct { 160 | // Test description. 161 | name string 162 | // Receiver fields. 163 | rsvc kmsInterface 164 | rkeyID string 165 | rProvider EncryptionProvider 166 | // Parameters. 167 | data *EncryptedData 168 | // Expected results. 169 | want []byte 170 | wantErr error 171 | }{ 172 | { 173 | "Decrypt", 174 | &mockKms{keyID: "keyId"}, 175 | "keyId", 176 | // Use NopEncryptor for testing 177 | func(key []byte) (EncryptDecryptor, error) { 178 | return NopEncryptor{}, nil 179 | }, 180 | &EncryptedData{ 181 | Ciphertext: []byte("secret"), 182 | HMAC: []byte("--ignored--"), 183 | Type: KMSWrapped, 184 | Context: map[string]interface{}{ 185 | "kms_type": Nop, 186 | "kms_key": []byte("AAAA"), 187 | }, 188 | }, 189 | []byte("secret"), 190 | nil, 191 | }, 192 | { 193 | "KMS request error", 194 | &mockKms{keyID: "keyId", err: errMarker}, 195 | "keyId", 196 | // Use NopEncryptor for testing 197 | func(key []byte) (EncryptDecryptor, error) { 198 | return NopEncryptor{}, nil 199 | }, 200 | &EncryptedData{ 201 | Ciphertext: []byte("secret"), 202 | HMAC: []byte("--ignored--"), 203 | Type: KMSWrapped, 204 | Context: map[string]interface{}{ 205 | "kms_type": Nop, 206 | "kms_key": []byte("AAAA"), 207 | }, 208 | }, 209 | []byte("secret"), 210 | errMarker, 211 | }, 212 | { 213 | "Encryptor initalise errors passed up", 214 | &mockKms{keyID: "keyId"}, 215 | "keyId", 216 | // Use NopEncryptor for testing 217 | func(key []byte) (EncryptDecryptor, error) { 218 | return nil, errMarker 219 | }, 220 | &EncryptedData{ 221 | Ciphertext: []byte("secret"), 222 | HMAC: []byte("--ignored--"), 223 | Type: KMSWrapped, 224 | Context: map[string]interface{}{ 225 | "kms_type": Nop, 226 | "kms_key": []byte("AAAA"), 227 | }, 228 | }, 229 | []byte("secret"), 230 | errMarker, 231 | }, 232 | { 233 | "Encryptor Decrypt() errors passed up", 234 | &mockKms{keyID: "keyId"}, 235 | "keyId", 236 | // Use NopEncryptor for testing 237 | func(key []byte) (EncryptDecryptor, error) { 238 | return &errEncryptor{errMarker}, nil 239 | }, 240 | &EncryptedData{ 241 | Ciphertext: []byte("secret"), 242 | HMAC: []byte("--ignored--"), 243 | Type: KMSWrapped, 244 | Context: map[string]interface{}{ 245 | "kms_type": Nop, 246 | "kms_key": []byte("AAAA"), 247 | }, 248 | }, 249 | []byte("secret"), 250 | errMarker, 251 | }, 252 | { 253 | "Wrong type", 254 | &mockKms{keyID: "keyId"}, 255 | "keyId", 256 | // Use NopEncryptor for testing 257 | func(key []byte) (EncryptDecryptor, error) { 258 | return NopEncryptor{}, nil 259 | }, 260 | &EncryptedData{ 261 | Ciphertext: []byte("secret"), 262 | HMAC: []byte("--ignored--"), 263 | Type: Nop, 264 | Context: map[string]interface{}{ 265 | "kms_type": Nop, 266 | "kms_key": []byte("AAAA"), 267 | }, 268 | }, 269 | []byte("secret"), 270 | ErrWrongType, 271 | }, 272 | { 273 | "Missing kms_key", 274 | &mockKms{keyID: "keyId"}, 275 | "keyId", 276 | // Use NopEncryptor for testing 277 | func(key []byte) (EncryptDecryptor, error) { 278 | return NopEncryptor{}, nil 279 | }, 280 | &EncryptedData{ 281 | Ciphertext: []byte("secret"), 282 | HMAC: []byte("--ignored--"), 283 | Type: KMSWrapped, 284 | Context: map[string]interface{}{ 285 | "kms_type": Nop, 286 | }, 287 | }, 288 | []byte("secret"), 289 | ErrMissingContext, 290 | }, 291 | { 292 | "Wrong kms_key type", 293 | &mockKms{keyID: "keyId"}, 294 | "keyId", 295 | // Use NopEncryptor for testing 296 | func(key []byte) (EncryptDecryptor, error) { 297 | return NopEncryptor{}, nil 298 | }, 299 | &EncryptedData{ 300 | Ciphertext: []byte("secret"), 301 | HMAC: []byte("--ignored--"), 302 | Type: KMSWrapped, 303 | Context: map[string]interface{}{ 304 | "kms_type": Nop, 305 | "kms_key": "wrong", 306 | }, 307 | }, 308 | []byte("secret"), 309 | ErrMissingContext, 310 | }, 311 | { 312 | "Missing kms_type", 313 | &mockKms{keyID: "keyId"}, 314 | "keyId", 315 | // Use NopEncryptor for testing 316 | func(key []byte) (EncryptDecryptor, error) { 317 | return NopEncryptor{}, nil 318 | }, 319 | &EncryptedData{ 320 | Ciphertext: []byte("secret"), 321 | HMAC: []byte("--ignored--"), 322 | Type: KMSWrapped, 323 | Context: map[string]interface{}{ 324 | "kms_type": Nop, 325 | }, 326 | }, 327 | []byte("secret"), 328 | ErrMissingContext, 329 | }, 330 | { 331 | "Wrong kms_type type", 332 | &mockKms{keyID: "keyId"}, 333 | "keyId", 334 | // Use NopEncryptor for testing 335 | func(key []byte) (EncryptDecryptor, error) { 336 | return NopEncryptor{}, nil 337 | }, 338 | &EncryptedData{ 339 | Ciphertext: []byte("secret"), 340 | HMAC: []byte("--ignored--"), 341 | Type: KMSWrapped, 342 | Context: map[string]interface{}{ 343 | "kms_type": "wrong", 344 | "kms_key": Nop, 345 | }, 346 | }, 347 | []byte("secret"), 348 | ErrMissingContext, 349 | }, 350 | } 351 | for _, tt := range tests { 352 | e := &KMS{ 353 | svc: tt.rsvc, 354 | keyID: tt.rkeyID, 355 | KeySize: 64, 356 | Provider: tt.rProvider, 357 | } 358 | got, err := e.Decrypt(tt.data) 359 | if err != tt.wantErr { 360 | t.Errorf("%q. KMS.Decrypt() error = %v, wantErr %v", tt.name, err, tt.wantErr) 361 | continue 362 | } 363 | 364 | if err != nil { 365 | continue 366 | } 367 | 368 | if !reflect.DeepEqual(got, tt.want) { 369 | t.Errorf("%q. KMS.Decrypt() = %v, want %v", tt.name, got, tt.want) 370 | } 371 | 372 | if tt.data.Type != KMSWrapped { 373 | t.Errorf("%q. KMS.Decrypt() mutated input, type = %v, want %v", tt.name, tt.data.Type, KMSWrapped) 374 | } 375 | } 376 | } 377 | 378 | func TestKMSIntegration(t *testing.T) { 379 | tests := []struct { 380 | // Test description. 381 | name string 382 | // Receiver fields. 383 | rsvc kmsInterface 384 | rkeyID string 385 | rProvider EncryptionProvider 386 | // Parameters. 387 | secret []byte 388 | // Expected results. 389 | wantEncryptErr error 390 | wantDecryptErr error 391 | }{ 392 | { 393 | "Decrypt", 394 | &mockKms{keyID: "keyId"}, 395 | "keyId", 396 | func(key []byte) (EncryptDecryptor, error) { 397 | return NewAES(key[:32], key[32:]) 398 | }, 399 | []byte("secret"), 400 | nil, 401 | nil, 402 | }, 403 | } 404 | for _, tt := range tests { 405 | e := &KMS{ 406 | svc: tt.rsvc, 407 | keyID: tt.rkeyID, 408 | KeySize: 64, 409 | Provider: tt.rProvider, 410 | } 411 | 412 | encd, err := e.Encrypt(tt.secret) 413 | if err != tt.wantEncryptErr { 414 | t.Errorf("%q. KMS.Encrypt() error = %v, wantErr %v", tt.name, err, tt.wantEncryptErr) 415 | continue 416 | } 417 | 418 | got, err := e.Decrypt(encd) 419 | if err != tt.wantDecryptErr { 420 | t.Errorf("%q. KMS.Decrypt() error = %v, wantErr %v", tt.name, err, tt.wantDecryptErr) 421 | continue 422 | } 423 | 424 | if !bytes.Equal(got, tt.secret) { 425 | t.Errorf("%q. KMS.Decrypt() = %v, want %v", tt.name, got, tt.secret) 426 | } 427 | } 428 | } 429 | -------------------------------------------------------------------------------- /encryptor/nop_encryptor.go: -------------------------------------------------------------------------------- 1 | package encryptor 2 | 3 | // NopEncryptor returns a EncryptedData struct that's not encrypted in any way 4 | // for development purposes. 5 | // 6 | // It does nothing! It should not be used for production, obviously. 7 | type NopEncryptor struct{} 8 | 9 | // Encrypt does nothing! It simply returns an initalised EncryptedData struct 10 | // with NO ENCRYPTION. 11 | // 12 | // Don't use it for anything other than tests. Seriously. 13 | func (e NopEncryptor) Encrypt(secret []byte) (*EncryptedData, error) { 14 | return &EncryptedData{ 15 | Ciphertext: secret, 16 | HMAC: []byte("--ignored--"), 17 | Type: Nop, 18 | }, nil 19 | } 20 | 21 | // Decrypt extracts the plain-text secret from data 22 | func (e NopEncryptor) Decrypt(data *EncryptedData) ([]byte, error) { 23 | return data.Ciphertext, nil 24 | } 25 | -------------------------------------------------------------------------------- /encryptor/nop_encryptor_test.go: -------------------------------------------------------------------------------- 1 | package encryptor 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestNopEncryptorIntegration(t *testing.T) { 9 | tests := []struct { 10 | // Test description. 11 | name string 12 | // Parameters. 13 | want []byte 14 | }{ 15 | { 16 | "Simple string", 17 | []byte("i am a secret"), 18 | }, 19 | { 20 | "Binary", 21 | []byte{0x42, 0x00, 0xDE, 0xAD, 0xBE, 0xEF}, 22 | }, 23 | } 24 | 25 | for _, tt := range tests { 26 | e := NopEncryptor{} 27 | 28 | encrypted, err := e.Encrypt(tt.want) 29 | if err != nil { 30 | t.Errorf("%q. Encrypt() = %s", tt.name, err) 31 | continue 32 | } 33 | 34 | got, err := e.Decrypt(encrypted) 35 | if err != nil { 36 | t.Errorf("%q. Decrypt() = %s", tt.name, err) 37 | continue 38 | } 39 | 40 | if !bytes.Equal(got, tt.want) { 41 | t.Errorf("%q. Secret mismatch, got %v, want %v", tt.name, string(got), string(tt.want)) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /encryptor/package.go: -------------------------------------------------------------------------------- 1 | // Package encryptor implements various encyption methods for use with cryptic. 2 | // 3 | // Everything in this package implements the EncryptDecryptor interface. 4 | // 5 | // See the KMS implementation for an example of how encryptors can be chained 6 | // together to create layered solutions. 7 | // 8 | // Where suitable, implementors of the EncryptDecryptor interface should return 9 | // the errors defined in this package. 10 | // 11 | // Encryptors must support binary secrets. 12 | package encryptor 13 | -------------------------------------------------------------------------------- /examples_test.go: -------------------------------------------------------------------------------- 1 | package cryptic 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/domodwyer/cryptic/encryptor" 7 | "github.com/domodwyer/cryptic/store" 8 | ) 9 | 10 | // Below is an example of how to use the library in your own Go programs. 11 | // 12 | // Note you should definitely be checking the error returns, they're omitted 13 | // here for brevity. 14 | func Example() { 15 | store := store.NewMemory() 16 | 17 | aesKey := []byte("anAesTestKey1234") 18 | hmacKey := []byte("superSecretHmacKey") 19 | 20 | // Note the AES key has to be either 16, 24, or 32 bytes 21 | e, _ := encryptor.NewAES(aesKey, hmacKey) 22 | 23 | // Encrypt the secret and store it 24 | result, _ := e.Encrypt([]byte("something secret")) 25 | store.Put("example", result) 26 | 27 | // 28 | // Time passes... 29 | // 30 | 31 | // Fetch and decrypt 32 | data, _ := store.Get("example") 33 | plain, _ := e.Decrypt(data) 34 | 35 | // Output 36 | fmt.Printf("%s\n", plain) 37 | // Output: something secret 38 | } 39 | -------------------------------------------------------------------------------- /package.go: -------------------------------------------------------------------------------- 1 | // Package cryptic provides a library for storage of encrypted secrets. 2 | // 3 | // It uses simple interfaces to define methods used for encryption and storage, 4 | // allowing the package to be easily extened. 5 | package cryptic 6 | -------------------------------------------------------------------------------- /store/db.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | 7 | "github.com/domodwyer/cryptic/encryptor" 8 | ) 9 | 10 | // DB stores secrets in a database. 11 | // 12 | // It is expected that the key column has a UNIQUE constraint. Unlike most 13 | // stores, DB does not return ErrAlreadyExists when attempting to Put() a secret 14 | // already in the store, as each database driver returns a different error - 15 | // instead the driver specific error is returned. 16 | type DB struct { 17 | getStmt *sql.Stmt 18 | putStmt *sql.Stmt 19 | delStmt *sql.Stmt 20 | } 21 | 22 | // DBOpts allows the user to use a different database schema than the defaults. 23 | // 24 | // It is expected that the DBOpts values are from trusted input (free from SQL 25 | // injection vectors). 26 | type DBOpts struct { 27 | Table string 28 | Key string 29 | Value string 30 | } 31 | 32 | // NewDB returns an initalised DB store 33 | func NewDB(db *sql.DB, opts *DBOpts) (*DB, error) { 34 | t, k, v := parseOpts(opts) 35 | 36 | getSQL := fmt.Sprintf("SELECT `%s` FROM `%s` WHERE `%s`= ? LIMIT 1", v, t, k) 37 | get, err := db.Prepare(getSQL) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | putSQL := fmt.Sprintf("INSERT INTO `%s` (`%s`, `%s`) VALUES (?, ?)", t, k, v) 43 | put, err := db.Prepare(putSQL) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | delSQL := fmt.Sprintf("DELETE FROM `%s` WHERE `%s` = ?", t, k) 49 | del, err := db.Prepare(delSQL) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | return &DB{get, put, del}, nil 55 | } 56 | 57 | // parseOpts sets sensible defaults, and returns any user-set DB config. 58 | func parseOpts(opts *DBOpts) (string, string, string) { 59 | t := "secrets" 60 | k := "name" 61 | v := "data" 62 | 63 | if opts == nil { 64 | return t, k, v 65 | } 66 | 67 | if opts.Table != "" { 68 | t = opts.Table 69 | } 70 | 71 | if opts.Key != "" { 72 | k = opts.Key 73 | } 74 | 75 | if opts.Value != "" { 76 | v = opts.Value 77 | } 78 | 79 | return t, k, v 80 | } 81 | 82 | // Put encodes data using binary gobs and stores the result in the database 83 | // using name as the key. 84 | func (s *DB) Put(name string, data *encryptor.EncryptedData) error { 85 | if name == "" { 86 | return ErrInvalidName 87 | } 88 | 89 | buf, err := data.MarshalBinary() 90 | if err != nil { 91 | return err 92 | } 93 | 94 | if _, err := s.putStmt.Exec(name, buf); err != nil { 95 | return err 96 | } 97 | 98 | return nil 99 | } 100 | 101 | // Get fetches the secret stored under name. 102 | func (s *DB) Get(name string) (*encryptor.EncryptedData, error) { 103 | if name == "" { 104 | return nil, ErrInvalidName 105 | } 106 | 107 | data := []byte{} 108 | 109 | // Get the data, translate a ErrNoRows into our ErrNotFound 110 | err := s.getStmt.QueryRow(name).Scan(&data) 111 | 112 | switch err { 113 | case nil: 114 | break 115 | 116 | case sql.ErrNoRows: 117 | return nil, ErrNotFound 118 | 119 | default: 120 | return nil, err 121 | } 122 | 123 | d := encryptor.EncryptedData{} 124 | if err := d.UnmarshalBinary(data); err != nil { 125 | return nil, err 126 | } 127 | 128 | return &d, nil 129 | } 130 | 131 | // Delete removes a secret from the database. 132 | func (s *DB) Delete(name string) error { 133 | if name == "" { 134 | return ErrInvalidName 135 | } 136 | 137 | res, err := s.delStmt.Exec(name) 138 | if err != nil { 139 | return err 140 | } 141 | 142 | i, err := res.RowsAffected() 143 | if err != nil { 144 | return err 145 | } 146 | 147 | if i < 1 { 148 | return ErrNotFound 149 | } 150 | 151 | return nil 152 | } 153 | -------------------------------------------------------------------------------- /store/db_test.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "database/sql" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/domodwyer/cryptic/encryptor" 9 | _ "github.com/mattn/go-sqlite3" 10 | ) 11 | 12 | const tableSQL = ` 13 | CREATE TABLE secrets( 14 | id INTEGER NOT NULL PRIMARY KEY, 15 | name TEXT UNIQUE, 16 | data BLOB 17 | ); 18 | ` 19 | 20 | // TestDbPutGet uses an in-memory sqlite DB to verify Get() returns the same as what 21 | // was Put() in. 22 | func TestDbPutGet(t *testing.T) { 23 | db, err := sql.Open("sqlite3", ":memory:") 24 | if err != nil { 25 | t.Errorf("Failed to set up sqlite db: %s", err) 26 | return 27 | } 28 | 29 | if _, err := db.Exec(tableSQL); err != nil { 30 | t.Errorf("Failed to create db table: %s", err) 31 | return 32 | } 33 | 34 | tests := []struct { 35 | // Test description. 36 | name string 37 | // Parameters. 38 | pname string 39 | pdata *encryptor.EncryptedData 40 | // Expected results. 41 | putErr error 42 | getErr error 43 | }{ 44 | { 45 | "Empty", 46 | "empty secret", 47 | &encryptor.EncryptedData{ 48 | Context: map[string]interface{}{}, 49 | }, 50 | nil, 51 | nil, 52 | }, 53 | { 54 | "No secret name", 55 | "", 56 | nil, 57 | ErrInvalidName, 58 | ErrInvalidName, 59 | }, 60 | { 61 | "Simple", 62 | "simple secret", 63 | &encryptor.EncryptedData{ 64 | Ciphertext: []byte("ciphertext"), 65 | HMAC: []byte("hmac"), 66 | Type: encryptor.Nop, 67 | Context: map[string]interface{}{}, 68 | }, 69 | nil, 70 | nil, 71 | }, 72 | { 73 | "Simple, binary", 74 | "binary secret", 75 | &encryptor.EncryptedData{ 76 | Ciphertext: []byte{0x42, 0x00, 0xDE, 0xAD, 0xBE, 0xEF}, 77 | HMAC: []byte{0xDE, 0xAD, 0xBA, 0xAD}, 78 | Type: encryptor.Nop, 79 | Context: map[string]interface{}{}, 80 | }, 81 | nil, 82 | nil, 83 | }, 84 | { 85 | "Simple, with context", 86 | "secret with context", 87 | &encryptor.EncryptedData{ 88 | Ciphertext: []byte("ciphertext"), 89 | HMAC: []byte("hmac"), 90 | Type: encryptor.Nop, 91 | Context: map[string]interface{}{ 92 | "string": "value", 93 | "int": 42, 94 | }, 95 | }, 96 | nil, 97 | nil, 98 | }, 99 | } 100 | 101 | for _, tt := range tests { 102 | s, err := NewDB(db, nil) 103 | if err != nil { 104 | t.Errorf("%q. NewDB() err = %v", tt.name, err) 105 | continue 106 | } 107 | 108 | if err := s.Put(tt.pname, tt.pdata); err != tt.putErr { 109 | t.Errorf("%q. DB.Put() err = %v, want %v", tt.name, err, tt.putErr) 110 | continue 111 | } 112 | 113 | got, err := s.Get(tt.pname) 114 | if err != tt.getErr { 115 | t.Errorf("%q. DB.Get() err = %v, want %v", tt.name, err, tt.getErr) 116 | continue 117 | } 118 | 119 | if !reflect.DeepEqual(got, tt.pdata) { 120 | t.Errorf("%q. DB.Get() = %v, want %v", tt.name, got, tt.pdata) 121 | } 122 | } 123 | } 124 | 125 | // TestDbNotFound ensures calling Get() for a secret that doesn't exist produces 126 | // an error 127 | func TestDbNotFound(t *testing.T) { 128 | db, err := sql.Open("sqlite3", ":memory:") 129 | if err != nil { 130 | t.Errorf("Failed to set up sqlite db: %s", err) 131 | return 132 | } 133 | 134 | if _, err := db.Exec(tableSQL); err != nil { 135 | t.Errorf("Failed to create db table: %s", err) 136 | return 137 | } 138 | 139 | s, err := NewDB(db, nil) 140 | if err != nil { 141 | t.Errorf("NotFound. NewDB() err = %v", err) 142 | return 143 | } 144 | 145 | got, err := s.Get("nope") 146 | if err != ErrNotFound { 147 | t.Errorf("NotFound. DB.Get() err = %v, want %v", err, ErrNotFound) 148 | return 149 | } 150 | 151 | if got != nil { 152 | t.Errorf("NotFound. DB.Get() got = %v, want %v", got, nil) 153 | return 154 | } 155 | } 156 | 157 | func TestDbParseOpts(t *testing.T) { 158 | tests := []struct { 159 | // Test description. 160 | name string 161 | // Parameters. 162 | opts *DBOpts 163 | // Expected results. 164 | want string 165 | want1 string 166 | want2 string 167 | }{ 168 | { 169 | "Defaults", 170 | &DBOpts{}, 171 | "secrets", 172 | "name", 173 | "data", 174 | }, 175 | { 176 | "Nil", 177 | nil, 178 | "secrets", 179 | "name", 180 | "data", 181 | }, 182 | { 183 | "Table", 184 | &DBOpts{Table: "test"}, 185 | "test", 186 | "name", 187 | "data", 188 | }, 189 | { 190 | "Key", 191 | &DBOpts{Key: "test"}, 192 | "secrets", 193 | "test", 194 | "data", 195 | }, 196 | { 197 | "Value", 198 | &DBOpts{Value: "test"}, 199 | "secrets", 200 | "name", 201 | "test", 202 | }, 203 | } 204 | for _, tt := range tests { 205 | got, got1, got2 := parseOpts(tt.opts) 206 | if got != tt.want { 207 | t.Errorf("%q. parseOpts() got = %v, want %v", tt.name, got, tt.want) 208 | } 209 | if got1 != tt.want1 { 210 | t.Errorf("%q. parseOpts() got1 = %v, want %v", tt.name, got1, tt.want1) 211 | } 212 | if got2 != tt.want2 { 213 | t.Errorf("%q. parseOpts() got2 = %v, want %v", tt.name, got2, tt.want2) 214 | } 215 | } 216 | } 217 | 218 | // TestDbDelete uses an in-memory sqlite DB to verify Delete() returns correct 219 | // errors, and removes things 220 | func TestDbDelete(t *testing.T) { 221 | db, err := sql.Open("sqlite3", ":memory:") 222 | if err != nil { 223 | t.Errorf("Failed to set up sqlite db: %s", err) 224 | return 225 | } 226 | 227 | if _, err := db.Exec(tableSQL); err != nil { 228 | t.Errorf("Failed to create db table: %s", err) 229 | return 230 | } 231 | 232 | existing := "INSERT INTO `secrets` (`name`, `data`) VALUES ('iExist', 'data');" 233 | if _, err := db.Exec(existing); err != nil { 234 | t.Errorf("Failed to insert known row: %s", err) 235 | return 236 | } 237 | 238 | tests := []struct { 239 | // Test description. 240 | name string 241 | // Parameters. 242 | pname string 243 | // Expected results. 244 | wantErr error 245 | }{ 246 | { 247 | "Not found", 248 | "missing", 249 | ErrNotFound, 250 | }, 251 | { 252 | "OK", 253 | "iExist", 254 | nil, 255 | }, 256 | { 257 | "Definitely gone", 258 | "iExist", 259 | ErrNotFound, 260 | }, 261 | { 262 | "No name", 263 | "", 264 | ErrInvalidName, 265 | }, 266 | } 267 | for _, tt := range tests { 268 | s, err := NewDB(db, nil) 269 | if err != nil { 270 | t.Errorf("NotFound. NewDB() err = %v", err) 271 | return 272 | } 273 | 274 | if err := s.Delete(tt.pname); err != tt.wantErr { 275 | t.Errorf("%q. DB.Delete() error = %v, wantErr %v", tt.name, err, tt.wantErr) 276 | } 277 | } 278 | } 279 | 280 | func TestDbPut(t *testing.T) { 281 | db, err := sql.Open("sqlite3", ":memory:") 282 | if err != nil { 283 | t.Errorf("Failed to set up sqlite db: %s", err) 284 | return 285 | } 286 | 287 | if _, err := db.Exec(tableSQL); err != nil { 288 | t.Errorf("Failed to create db table: %s", err) 289 | return 290 | } 291 | 292 | tests := []struct { 293 | // Test description. 294 | name string 295 | // Parameters. 296 | pname string 297 | // Expected results. 298 | wantErr bool 299 | }{ 300 | { 301 | "Simple", 302 | "simple", 303 | false, 304 | }, 305 | { 306 | "No overwrite", 307 | "simple", 308 | true, 309 | }, 310 | } 311 | for _, tt := range tests { 312 | s, err := NewDB(db, nil) 313 | if err != nil { 314 | t.Errorf("NotFound. NewDB() err = %v", err) 315 | return 316 | } 317 | 318 | if err := s.Put(tt.pname, &encryptor.EncryptedData{}); (err != nil) != tt.wantErr { 319 | t.Errorf("%q. DB.Put() error = %v, wantErr %v", tt.name, err, tt.wantErr) 320 | } 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /store/errors.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import "errors" 4 | 5 | var ( 6 | // ErrNotFound is returned when a secret is not found in the store. 7 | ErrNotFound = errors.New("store: secret not found") 8 | 9 | // ErrInvalidName is returned when a name isn't supported (or is ridiculous, like ""). 10 | ErrInvalidName = errors.New("store: invalid secret name") 11 | 12 | // ErrAlreadyExists is returned when attempting to Put() a secret with the 13 | // same name as an existing entry. 14 | ErrAlreadyExists = errors.New("store: secret already exists") 15 | ) 16 | -------------------------------------------------------------------------------- /store/memory.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/domodwyer/cryptic/encryptor" 7 | ) 8 | 9 | type rwLocker interface { 10 | sync.Locker 11 | RLock() 12 | RUnlock() 13 | } 14 | 15 | // Memory is an in-memory data store. Contents are not persisted in any way 16 | // after the process ends. 17 | type Memory struct { 18 | secrets map[string]encryptor.EncryptedData 19 | mu rwLocker 20 | } 21 | 22 | // NewMemory returns an initalised memory store. 23 | func NewMemory() *Memory { 24 | return &Memory{ 25 | secrets: map[string]encryptor.EncryptedData{}, 26 | mu: &sync.RWMutex{}, 27 | } 28 | } 29 | 30 | // Put stores data under the given name. 31 | func (s *Memory) Put(name string, data *encryptor.EncryptedData) error { 32 | if name == "" { 33 | return ErrInvalidName 34 | } 35 | 36 | if _, err := s.Get(name); err != ErrNotFound { 37 | return ErrAlreadyExists 38 | } 39 | 40 | s.mu.Lock() 41 | defer s.mu.Unlock() 42 | 43 | s.secrets[name] = *data 44 | return nil 45 | } 46 | 47 | // Get fetches the EncryptedData stored under name. 48 | func (s *Memory) Get(name string) (*encryptor.EncryptedData, error) { 49 | if name == "" { 50 | return nil, ErrInvalidName 51 | } 52 | 53 | s.mu.RLock() 54 | defer s.mu.RUnlock() 55 | 56 | d, ok := s.secrets[name] 57 | if !ok { 58 | return nil, ErrNotFound 59 | } 60 | 61 | return &d, nil 62 | } 63 | 64 | // Delete removes a secret from the memory store. 65 | func (s *Memory) Delete(name string) error { 66 | if name == "" { 67 | return ErrInvalidName 68 | } 69 | 70 | s.mu.Lock() 71 | defer s.mu.Unlock() 72 | 73 | if _, ok := s.secrets[name]; !ok { 74 | return ErrNotFound 75 | } 76 | 77 | delete(s.secrets, name) 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /store/memory_test.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/domodwyer/cryptic/encryptor" 8 | ) 9 | 10 | // mockLock is used to check the lock state after our methods return 11 | type mockLock struct { 12 | isLocked bool 13 | isRLocked bool 14 | } 15 | 16 | func (m *mockLock) Lock() { m.isLocked = true } 17 | func (m *mockLock) Unlock() { m.isLocked = false } 18 | func (m *mockLock) RLock() { m.isRLocked = true } 19 | func (m *mockLock) RUnlock() { m.isRLocked = false } 20 | 21 | // TestMemoryGet ensures we're developing with a in-memory store that actually 22 | // works 23 | func TestMemoryGet(t *testing.T) { 24 | tests := []struct { 25 | // Test description. 26 | name string 27 | // Receiver fields. 28 | rsecrets map[string]encryptor.EncryptedData 29 | // Parameters. 30 | pname string 31 | // Expected results. 32 | want *encryptor.EncryptedData 33 | wantErr error 34 | }{ 35 | { 36 | "Empty == ErrNotFound", 37 | map[string]encryptor.EncryptedData{}, 38 | "missing", 39 | nil, 40 | ErrNotFound, 41 | }, 42 | { 43 | "No name", 44 | map[string]encryptor.EncryptedData{}, 45 | "", 46 | nil, 47 | ErrInvalidName, 48 | }, 49 | { 50 | "Some entries, not this one", 51 | map[string]encryptor.EncryptedData{ 52 | "kings": {Ciphertext: []byte("a 🐐")}, 53 | "bugs": {Ciphertext: []byte("oh noes! 💣")}, 54 | }, 55 | "missing", 56 | nil, 57 | ErrNotFound, 58 | }, 59 | { 60 | "Get a goat", 61 | map[string]encryptor.EncryptedData{ 62 | "kings": {Ciphertext: []byte("a 🐐")}, 63 | "bugs": {Ciphertext: []byte("oh noes! 💣")}, 64 | }, 65 | "kings", 66 | &encryptor.EncryptedData{Ciphertext: []byte("a 🐐")}, 67 | nil, 68 | }, 69 | } 70 | for _, tt := range tests { 71 | lock := &mockLock{} 72 | 73 | b := &Memory{ 74 | secrets: tt.rsecrets, 75 | mu: lock, 76 | } 77 | 78 | got, err := b.Get(tt.pname) 79 | if err != tt.wantErr { 80 | t.Errorf("%q. Memory.Get() error = %v, wantErr %v", tt.name, err, tt.wantErr) 81 | continue 82 | } 83 | if !reflect.DeepEqual(got, tt.want) { 84 | t.Errorf("%q. Memory.Get() = %v, want %v", tt.name, got, tt.want) 85 | } 86 | 87 | // Ensure we always release our locks 88 | if lock.isLocked { 89 | t.Errorf("%q. Memory.Get() is still locked!", tt.name) 90 | } 91 | 92 | if lock.isRLocked { 93 | t.Errorf("%q. Memory.Get() is still read locked!", tt.name) 94 | } 95 | } 96 | } 97 | 98 | // TestMemoryDelete ensures correct errors are returned, and entries are 99 | // actually removed 100 | func TestMemoryDelete(t *testing.T) { 101 | tests := []struct { 102 | // Test description. 103 | name string 104 | // Receiver fields. 105 | rsecrets map[string]encryptor.EncryptedData 106 | // Parameters. 107 | pname string 108 | // Expected results. 109 | want map[string]encryptor.EncryptedData 110 | wantErr error 111 | }{ 112 | { 113 | "Empty == ErrNotFound", 114 | map[string]encryptor.EncryptedData{}, 115 | "missing", 116 | map[string]encryptor.EncryptedData{}, 117 | ErrNotFound, 118 | }, 119 | { 120 | "Some entries, not this one", 121 | map[string]encryptor.EncryptedData{ 122 | "kings": {Ciphertext: []byte("a 🐐")}, 123 | "bugs": {Ciphertext: []byte("oh noes! 💣")}, 124 | }, 125 | "missing", 126 | map[string]encryptor.EncryptedData{ 127 | "kings": {Ciphertext: []byte("a 🐐")}, 128 | "bugs": {Ciphertext: []byte("oh noes! 💣")}, 129 | }, 130 | ErrNotFound, 131 | }, 132 | { 133 | "Get a goat", 134 | map[string]encryptor.EncryptedData{ 135 | "kings": {Ciphertext: []byte("a 🐐")}, 136 | "bugs": {Ciphertext: []byte("oh noes! 💣")}, 137 | }, 138 | "kings", 139 | map[string]encryptor.EncryptedData{ 140 | "bugs": {Ciphertext: []byte("oh noes! 💣")}, 141 | }, 142 | nil, 143 | }, 144 | } 145 | for _, tt := range tests { 146 | lock := &mockLock{} 147 | 148 | b := &Memory{ 149 | secrets: tt.rsecrets, 150 | mu: lock, 151 | } 152 | 153 | if err := b.Delete(tt.pname); err != tt.wantErr { 154 | t.Errorf("%q. Memory.Delete() error = %v, wantErr %v", tt.name, err, tt.wantErr) 155 | continue 156 | } 157 | if !reflect.DeepEqual(tt.rsecrets, tt.want) { 158 | t.Errorf("%q. Memory.Delete() = %v, want %v", tt.name, tt.rsecrets, tt.want) 159 | } 160 | 161 | // Ensure we always release our locks 162 | if lock.isLocked { 163 | t.Errorf("%q. Memory.Delete() is still locked!", tt.name) 164 | } 165 | 166 | if lock.isRLocked { 167 | t.Errorf("%q. Memory.Delete() is still read locked!", tt.name) 168 | } 169 | } 170 | } 171 | 172 | func TestMemoryPut(t *testing.T) { 173 | tests := []struct { 174 | // Test description. 175 | name string 176 | // Receiver fields. 177 | rsecrets map[string]encryptor.EncryptedData 178 | // Parameters. 179 | pname string 180 | data *encryptor.EncryptedData 181 | // Expected results. 182 | want map[string]encryptor.EncryptedData 183 | wantErr error 184 | }{ 185 | { 186 | "Single", 187 | map[string]encryptor.EncryptedData{}, 188 | "test", 189 | &encryptor.EncryptedData{}, 190 | map[string]encryptor.EncryptedData{ 191 | "test": {}, 192 | }, 193 | nil, 194 | }, 195 | { 196 | "No overwrite", 197 | map[string]encryptor.EncryptedData{ 198 | "test": {Ciphertext: []byte("marker")}, 199 | }, 200 | "test", 201 | &encryptor.EncryptedData{Ciphertext: []byte("newVal")}, 202 | map[string]encryptor.EncryptedData{ 203 | "test": {Ciphertext: []byte("marker")}, 204 | }, 205 | ErrAlreadyExists, 206 | }, 207 | } 208 | for _, tt := range tests { 209 | lock := &mockLock{} 210 | 211 | s := &Memory{ 212 | secrets: tt.rsecrets, 213 | mu: lock, 214 | } 215 | 216 | err := s.Put(tt.pname, tt.data) 217 | if err != tt.wantErr { 218 | t.Errorf("%q. Memory.Put() error = %v, wantErr %v", tt.name, err, tt.wantErr) 219 | } 220 | 221 | if err != nil { 222 | continue 223 | } 224 | 225 | if !reflect.DeepEqual(tt.want, s.secrets) { 226 | t.Errorf("%q. Memory.Put() = %v, want %v", tt.name, s.secrets, tt.want) 227 | } 228 | 229 | // Ensure we always release our locks 230 | if lock.isLocked { 231 | t.Errorf("%q. Memory.Get() is still locked!", tt.name) 232 | } 233 | 234 | if lock.isRLocked { 235 | t.Errorf("%q. Memory.Get() is still read locked!", tt.name) 236 | } 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /store/package.go: -------------------------------------------------------------------------------- 1 | // Package store provides swappable backends that can store secrets generated by 2 | // the encryptor package. 3 | // 4 | // Where suitable, secret stores should return error codes defined in this 5 | // package. 6 | // 7 | // Stores must use whatever encoding scheme is required to safely store binary 8 | // data in the backend - i.e. Base64 if the backend only supports text, gobs if 9 | // it supports binary, etc. 10 | package store 11 | -------------------------------------------------------------------------------- /store/redis.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/domodwyer/cryptic/encryptor" 7 | 8 | "gopkg.in/redis.v4" 9 | ) 10 | 11 | // Redis abstracts storing secrets in a redis backend. 12 | // 13 | // If you're using a cluster of redis servers, you can initalise a Redis struct 14 | // directly and pass in an initalised Client using redis.NewFailoverClient. 15 | type Redis struct { 16 | Redis redisInterface 17 | } 18 | 19 | type redisInterface interface { 20 | Get(key string) *redis.StringCmd 21 | Set(key string, value interface{}, expiration time.Duration) *redis.StatusCmd 22 | Del(keys ...string) *redis.IntCmd 23 | } 24 | 25 | // NewRedis returns an initalised Redis store. 26 | func NewRedis(opts *redis.Options) *Redis { 27 | return &Redis{ 28 | Redis: redis.NewClient(opts), 29 | } 30 | } 31 | 32 | // Put stores the given secret in redis, with no expiration set. 33 | func (s *Redis) Put(name string, data *encryptor.EncryptedData) error { 34 | if name == "" { 35 | return ErrInvalidName 36 | } 37 | 38 | // TODO: the Get() and Put() should be done in a transaction using the redis 39 | // WATCH command to avoid race conditions - opportunistic locking. 40 | 41 | // Ensure we're not overwriting something 42 | if _, err := s.Get(name); err != ErrNotFound { 43 | return ErrAlreadyExists 44 | } 45 | 46 | // Because EncryptedData implements BinaryMarshaller, we can pass it 47 | // directly to redis - the redis library will marshal it for us. 48 | if err := s.Redis.Set(name, data, 0).Err(); err != nil { 49 | return err 50 | } 51 | 52 | return nil 53 | } 54 | 55 | // Get fetches the secret from redis. 56 | func (s *Redis) Get(name string) (*encryptor.EncryptedData, error) { 57 | if name == "" { 58 | return nil, ErrInvalidName 59 | } 60 | 61 | resp := s.Redis.Get(name) 62 | if err := resp.Err(); err != nil { 63 | // Check if it's the (annoyingly) unexported "redis: nil" error by 64 | // checking the byte length. No bytes, no results. 65 | b, _ := resp.Bytes() 66 | if len(b) < 1 { 67 | return nil, ErrNotFound 68 | } 69 | 70 | return nil, err 71 | } 72 | 73 | // Let the redis library unmarshal our data, because EncryptedData 74 | // implements BinaryUnmarshaler 75 | d := &encryptor.EncryptedData{} 76 | if err := resp.Scan(d); err != nil { 77 | return nil, err 78 | } 79 | 80 | return d, nil 81 | } 82 | 83 | // Delete removes the secret from redis. 84 | func (s *Redis) Delete(name string) error { 85 | // TODO: Transaction same as Put() 86 | if _, err := s.Get(name); err != nil { 87 | return ErrNotFound 88 | } 89 | 90 | resp := s.Redis.Del(name) 91 | if err := resp.Err(); err != nil { 92 | return err 93 | } 94 | 95 | return nil 96 | } 97 | -------------------------------------------------------------------------------- /store/redis_test.go: -------------------------------------------------------------------------------- 1 | // +build integration 2 | 3 | package store 4 | 5 | import ( 6 | "os" 7 | "reflect" 8 | "testing" 9 | 10 | "github.com/domodwyer/cryptic/encryptor" 11 | "gopkg.in/redis.v4" 12 | ) 13 | 14 | // Because go-redis returns thing such as redis.StatusCmd{} with unexported 15 | // fields, we cannot really mock out the redis client and get it to do what we 16 | // want, therefore we have to use a live redis instance. Maybe go-redis should 17 | // return an interface? 18 | 19 | func TestRedisPut(t *testing.T) { 20 | 21 | // If we don't have a host to connect to, skip all the redis integration 22 | // tests 23 | if os.Getenv("REDIS_HOST") == "" { 24 | t.Skip("no REDIS_HOST environment variable set, skipping integration tests") 25 | } 26 | 27 | c := redis.NewClient(&redis.Options{ 28 | Addr: os.Getenv("REDIS_HOST"), 29 | }) 30 | 31 | // Remove used test keys 32 | if err := c.Del("integration_test_key").Err(); err != nil { 33 | t.Errorf("redis: removing test keys: %s", err) 34 | return 35 | } 36 | 37 | tests := []struct { 38 | // Test description. 39 | name string 40 | // Parameters. 41 | pname string 42 | data *encryptor.EncryptedData 43 | // Expected results. 44 | wantErr error 45 | }{ 46 | { 47 | "Example", 48 | "integration_test_key", 49 | &encryptor.EncryptedData{}, 50 | nil, 51 | }, 52 | { 53 | "No overwrite", 54 | "integration_test_key", 55 | &encryptor.EncryptedData{}, 56 | ErrAlreadyExists, 57 | }, 58 | { 59 | "No name", 60 | "", 61 | &encryptor.EncryptedData{}, 62 | ErrInvalidName, 63 | }, 64 | } 65 | for _, tt := range tests { 66 | s := Redis{Redis: c} 67 | 68 | if err := s.Put(tt.pname, tt.data); err != tt.wantErr { 69 | t.Errorf("%q. Redis.Put() error = %v, wantErr %v", tt.name, err, tt.wantErr) 70 | } 71 | } 72 | } 73 | 74 | func TestRedisGet(t *testing.T) { 75 | 76 | // If we don't have a host to connect to, skip all the redis integration 77 | // tests 78 | if os.Getenv("REDIS_HOST") == "" { 79 | t.Skip("no REDIS_HOST environment variable set, skipping integration tests") 80 | } 81 | 82 | c := redis.NewClient(&redis.Options{ 83 | Addr: os.Getenv("REDIS_HOST"), 84 | }) 85 | 86 | s := Redis{Redis: c} 87 | if err := s.Put("integration_test_key2", &encryptor.EncryptedData{Ciphertext: []byte("marker")}); err != nil { 88 | t.Errorf("redis: setting up integration test key: %s", err) 89 | return 90 | } 91 | 92 | tests := []struct { 93 | // Test description. 94 | name string 95 | // Parameters. 96 | pname string 97 | // Expected results. 98 | want *encryptor.EncryptedData 99 | wantErr error 100 | }{ 101 | { 102 | "Example", 103 | "integration_test_key2", 104 | &encryptor.EncryptedData{ 105 | Ciphertext: []byte("marker"), 106 | Context: map[string]interface{}{}, 107 | }, 108 | nil, 109 | }, 110 | { 111 | "Not found", 112 | "integration_test_key_missing", 113 | nil, 114 | ErrNotFound, 115 | }, 116 | { 117 | "No name", 118 | "", 119 | nil, 120 | ErrInvalidName, 121 | }, 122 | } 123 | 124 | for _, tt := range tests { 125 | got, err := s.Get(tt.pname) 126 | if err != tt.wantErr { 127 | t.Errorf("%q. Redis.Get() error = %v, wantErr %v", tt.name, err, tt.wantErr) 128 | continue 129 | } 130 | if !reflect.DeepEqual(got, tt.want) { 131 | t.Errorf("%q. Redis.Get() = %v, want %v", tt.name, got, tt.want) 132 | } 133 | } 134 | 135 | // Remove used test keys 136 | if err := c.Del("integration_test_key2").Err(); err != nil { 137 | t.Errorf("redis: removing test keys: %s", err) 138 | return 139 | } 140 | } 141 | 142 | func TestRedisDelete(t *testing.T) { 143 | 144 | // If we don't have a host to connect to, skip all the redis integration 145 | // tests 146 | if os.Getenv("REDIS_HOST") == "" { 147 | t.Skip("no REDIS_HOST environment variable set, skipping integration tests") 148 | } 149 | 150 | c := redis.NewClient(&redis.Options{ 151 | Addr: os.Getenv("REDIS_HOST"), 152 | }) 153 | 154 | s := Redis{Redis: c} 155 | if err := s.Put("integration_test_key3", &encryptor.EncryptedData{Ciphertext: []byte("marker")}); err != nil { 156 | t.Errorf("redis: setting up integration test key: %s", err) 157 | return 158 | } 159 | 160 | tests := []struct { 161 | // Test description. 162 | name string 163 | // Parameters. 164 | pname string 165 | // Expected results. 166 | wantErr error 167 | }{ 168 | { 169 | "Example", 170 | "integration_test_key3", 171 | nil, 172 | }, 173 | { 174 | "Not found", 175 | "integration_test_key3", 176 | ErrNotFound, 177 | }, 178 | } 179 | for _, tt := range tests { 180 | if err := s.Delete(tt.pname); err != tt.wantErr { 181 | t.Errorf("%q. Redis.Delete() error = %v, wantErr %v", tt.name, err, tt.wantErr) 182 | } 183 | } 184 | 185 | // Remove used test keys 186 | if err := c.Del("integration_test_key3").Err(); err != nil { 187 | t.Errorf("redis: removing test keys: %s", err) 188 | return 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /store/store.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import "github.com/domodwyer/cryptic/encryptor" 4 | 5 | // Putter defines the interface for storing secrets in a back-end store. 6 | type Putter interface { 7 | Put(name string, data *encryptor.EncryptedData) error 8 | } 9 | 10 | // Getter defines the interface for fetching secrets from the back-end store. 11 | type Getter interface { 12 | Get(name string) (*encryptor.EncryptedData, error) 13 | } 14 | 15 | // Deleter defines the interface for deleting secrets from the back-end store. 16 | type Deleter interface { 17 | Delete(name string) error 18 | } 19 | 20 | // Interface combines the Putter, Getter and Deleter interface 21 | type Interface interface { 22 | Putter 23 | Getter 24 | Deleter 25 | } 26 | -------------------------------------------------------------------------------- /terraform/kms.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | access_key = "${var.aws_access_key}" 3 | secret_key = "${var.aws_secret_key}" 4 | region = "${var.aws_region}" 5 | } 6 | 7 | variable "aws_access_key" { 8 | description = "AWS access key" 9 | } 10 | 11 | variable "aws_secret_key" { 12 | description = "AWS secret access key" 13 | } 14 | 15 | variable "aws_region" { 16 | description = "AWS region" 17 | default = "eu-west-1" 18 | } 19 | 20 | resource "aws_kms_key" "cryptic_key" { 21 | description = "Cryptic secret store key" 22 | } 23 | 24 | output "kms_key_id" { 25 | value = "${aws_kms_key.cryptic_key.id}" 26 | } --------------------------------------------------------------------------------