├── .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 | [](https://travis-ci.org/domodwyer/cryptic) [](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 | }
--------------------------------------------------------------------------------