├── sum256.go ├── vault ├── doc.go ├── hashi │ ├── doc.go │ ├── vault.go │ └── vault_test.go └── vault.go ├── .gitignore ├── examples_test.go ├── LICENSE ├── key_provider.go ├── salt_provider.go ├── doc.go ├── key.go ├── README.md ├── crypto.go ├── key_test.go └── crypto_test.go /sum256.go: -------------------------------------------------------------------------------- 1 | package superdog 2 | 3 | import ( 4 | "crypto/sha256" 5 | "fmt" 6 | ) 7 | 8 | func Sum256String(s string) string { 9 | return fmt.Sprintf("%x", sha256.Sum256([]byte(s))) 10 | } 11 | -------------------------------------------------------------------------------- /vault/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | See LICENSE file for license details 3 | Copyright (c) 2015 XOR Data Exchange, Inc. 4 | 5 | 6 | Package vault provides an interface wrapping a secret store. 7 | */ 8 | package vault 9 | -------------------------------------------------------------------------------- /vault/hashi/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | See LICENSE file for license details 3 | Copyright (c) 2015 XOR Data Exchange, Inc. 4 | 5 | 6 | Package hashi provides an implementation of the vault interface for Hashicorp's Vault 7 | */ 8 | package hashi 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /examples_test.go: -------------------------------------------------------------------------------- 1 | package superdog_test 2 | 3 | import "github.com/xordataexchange/superdog" 4 | 5 | func ExampleEncrypt(prefix string, val []byte) []byte { 6 | encrypted, err := superdog.Encrypt(prefix, val, val) 7 | if err != nil { 8 | panic(err) 9 | } 10 | return encrypted 11 | } 12 | 13 | func ExampleDecrypt(prefix string, val []byte) []byte { 14 | 15 | decrypted, err := superdog.Decrypt(prefix, val, val) 16 | if err != nil { 17 | panic(err) 18 | } 19 | return decrypted 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 XOR Data Exchange, Inc. 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. 22 | 23 | -------------------------------------------------------------------------------- /key_provider.go: -------------------------------------------------------------------------------- 1 | package superdog 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "strconv" 7 | ) 8 | 9 | var ( 10 | ErrKeyNotFound = errors.New("Key not found") 11 | ) 12 | 13 | var _ KeyProvider = &DevKeyProvider{} 14 | 15 | // KeyProvider is an interface that wraps the GetKey method, responsible for retrieving encryption keys at a specified version. 16 | type KeyProvider interface { 17 | GetKey(prefix string, version uint64) (*Key, error) 18 | CurrentKeyVersion(prefix string) (uint64, error) 19 | } 20 | 21 | // DevKeyProvider is a KeyProvider used for development purposes only, and contains a hardcoded key. 22 | type DevKeyProvider struct { 23 | DisableWarn bool // Disable log messages whenever this provider is used. 24 | KeyVersion uint64 25 | } 26 | 27 | // CurrentKeyVersion returns the version number of the latest key for a given prefix 28 | func (kp *DevKeyProvider) CurrentKeyVersion(prefix string) (uint64, error) { 29 | return kp.KeyVersion, nil 30 | } 31 | 32 | func (kp *DevKeyProvider) GetKey(prefix string, version uint64) (*Key, error) { 33 | if !kp.DisableWarn { 34 | log.Println("USING DEV KEY PROVIDER!") 35 | } 36 | 37 | if version == 1 { 38 | return NewKey(version, AES, CFB, []byte("DEFAULT XOR KEY DEFAULT XOR KEY ")) 39 | } 40 | v := strconv.FormatUint(version, 10) 41 | return NewKey(version, AES, GCM, []byte("DEFAULT XOR KEY DEFAULT XOR KEY"+v)) 42 | } 43 | -------------------------------------------------------------------------------- /salt_provider.go: -------------------------------------------------------------------------------- 1 | package superdog 2 | 3 | import ( 4 | "log" 5 | "strconv" 6 | ) 7 | 8 | var _ SaltProvider = &DevSaltProvider{} 9 | 10 | type SaltProvider interface { 11 | CurrentSalts(prefix string) ([]uint64, error) 12 | GetSalt(prefix string, version uint64) ([]byte, error) 13 | CurrentSaltVersion(prefix string) (uint64, error) 14 | } 15 | 16 | // DevSaltProvider is a KeyProvider used for development purposes only, and contains a hardcoded key. 17 | type DevSaltProvider struct { 18 | DisableWarn bool // Disable log messages whenever this provider is used. 19 | SaltVersion uint64 20 | } 21 | 22 | // CurrentSaltVersion returns the version number of the latest salt for a given prefix 23 | func (sp *DevSaltProvider) CurrentSaltVersion(prefix string) (uint64, error) { 24 | return sp.SaltVersion, nil 25 | 26 | } 27 | 28 | // CurrentSalts returns a stubbed list of salts to be used for the given prefix 29 | func (sp *DevSaltProvider) CurrentSalts(prefix string) ([]uint64, error) { 30 | if !sp.DisableWarn { 31 | log.Println("USING DEV SALT PROVIDER!") 32 | } 33 | return []uint64{1, 2, 3}, nil 34 | } 35 | 36 | // GetSalt returns a stubbed salt to be used for the given prefix 37 | func (sp *DevSaltProvider) GetSalt(prefix string, version uint64) ([]byte, error) { 38 | if !sp.DisableWarn { 39 | log.Println("USING DEV SALT PROVIDER!") 40 | } 41 | return []byte("DEV SALT " + prefix + " " + strconv.FormatUint(version, 10)), nil 42 | } 43 | -------------------------------------------------------------------------------- /vault/vault.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "log" 5 | "strconv" 6 | 7 | "github.com/xordataexchange/superdog" 8 | ) 9 | 10 | // Vault is an interface wrapping a store for secrets. 11 | type Vault interface { 12 | superdog.KeyProvider 13 | superdog.SaltProvider 14 | } 15 | 16 | var _ Vault = &DevVault{} 17 | 18 | // DevVault returns hardcoded stubbed responses to remove development dependencies from a true vault. Insecure, do not use in production. 19 | type DevVault struct { 20 | DisableWarn bool // Disable log messages when this is used 21 | SaltVersion uint64 22 | KeyVersion uint64 23 | } 24 | 25 | func (v *DevVault) CurrentKeyVersion(prefix string) (uint64, error) { 26 | return v.KeyVersion, nil 27 | } 28 | 29 | func (v *DevVault) CurrentSaltVersion(prefix string) (uint64, error) { 30 | return v.SaltVersion, nil 31 | } 32 | 33 | // GetKey returns a stubbed encryption key for development purposes. 34 | func (v *DevVault) GetKey(prefix string, version uint64) (*superdog.Key, error) { 35 | if !v.DisableWarn { 36 | log.Println("USING DEV KEY PROVIDER!") 37 | } 38 | 39 | if version == 1 { 40 | return superdog.NewKey(version, superdog.AES, superdog.CFB, []byte("DEFAULT KEY ")) 41 | } 42 | 43 | vs := strconv.FormatUint(version, 10) 44 | return superdog.NewKey(version, superdog.AES, superdog.GCM, []byte("DEFAULT KEY DEFAULT KEY"+vs)) 45 | } 46 | 47 | // CurrentSalts returns a stubbed list of salts to be used for the given prefix 48 | func (v *DevVault) CurrentSalts(prefix string) ([]uint64, error) { 49 | if !v.DisableWarn { 50 | log.Println("USING DEV SALT PROVIDER!") 51 | } 52 | 53 | return []uint64{v.KeyVersion}, nil 54 | } 55 | 56 | // GetSalt returns a stubbed salt to be used for the given prefix 57 | func (v *DevVault) GetSalt(prefix string, version uint64) ([]byte, error) { 58 | if !v.DisableWarn { 59 | log.Println("USING DEV SALT PROVIDER!") 60 | } 61 | return []byte("DEV SALT " + prefix), nil 62 | } 63 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | See LICENSE file for license details 3 | Copyright (c) 2015 XOR Data Exchange, Inc. 4 | 5 | 6 | Superdog - the Crypto library for Vault from Hashicorp 7 | 8 | 9 | Superdog is a library for managing strong cryptography in both development and test environments. Superdog provides an elegant wrapper to the Vault(https://www.vaultproject.io) API that allows you to manage your cryptographic keys in Vault using any code that implements the `KeyProvider` interface. An implemention of the `KeyProvider` interface is provided for Vault, but others could be supported. 10 | 11 | Features 12 | 13 | - Versioned Keys - Key version is stored as the first few bytes of the encrypted text 14 | - Key Rotation - Rotate your keys safely, knowing that you'll always be able to decrypt older versionss 15 | - Development implementation for tests and local development 16 | - Versioned and Rotated IV/Salt - `SaltProvider` interface works the same as `KeyProvider` to allow development and testing access to the crypto libraries without requiring a live Key (Vault) server 17 | - `Reencrypt` function to simplify key rotation, decrypts with given key, reencrypts with latest key 18 | 19 | Cypher Suites 20 | 21 | `superdog` supports AES encryption with CFB/CTR/GCM/OFB modes. 22 | 23 | 24 | Production Usage 25 | By default, `superdog` uses the `DevKeyProvider` which is a static key with static IV. This is extremely insecure, and SHOULD NOT ever be used in production. 26 | 27 | We reccommend using Go's [build tags](https://golang.org/pkg/go/build/) to enable strong cryptography in production usage. 28 | 29 | Create a file with your connection routines in the init() function. Add the build tag `// +build production` to the top of that file. Incomplete example: 30 | 31 | // +build production 32 | 33 | package main 34 | import ( 35 | "github.com/xordataexchange/superdog" 36 | "github.com/xordataexxchange/superdog/vault/hashi" 37 | "github.com/hashicorp/vault/api" 38 | ) 39 | 40 | // Assign each application a unique UUID 41 | // and use Vault's AppID authentication mechanism 42 | const ( 43 | appid = "SOME RANDOM UUID" 44 | ) 45 | 46 | func init() { 47 | user := os.Getenv("VAULT_USER") 48 | vaultaddr := os.Getenv("VAULT_ADDRESS") 49 | // TEST these for empty strings & handle appropriately in your code 50 | cfg:= api.DefaultConfig() 51 | cfg.Address = vaultaddr 52 | vault, err := hashi.NewVault(cfg) 53 | if err != nil { 54 | // handle appropriately 55 | } 56 | err = vault.AuthAppID(appid, user) 57 | if err != nil { 58 | // handle appropriately 59 | } 60 | crypto.DefaultKeyProvider = vault 61 | crypto.DefaultSaltProvider = vault 62 | } 63 | 64 | Now compile your program with `go build -tags production` to include this code. The `KeyProvider` will be set to use Vault. 65 | 66 | */ 67 | package superdog 68 | -------------------------------------------------------------------------------- /key.go: -------------------------------------------------------------------------------- 1 | package superdog 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "crypto/rand" 7 | "encoding/binary" 8 | "errors" 9 | "io" 10 | ) 11 | 12 | type CipherBlockMode uint8 13 | type Cipher uint8 14 | 15 | const ( 16 | CFB CipherBlockMode = iota 17 | CTR 18 | OFB 19 | GCM 20 | ) 21 | 22 | const ( 23 | AES Cipher = iota 24 | ) 25 | 26 | type Key struct { 27 | Cipher Cipher 28 | CipherBlockMode CipherBlockMode 29 | block cipher.Block 30 | ivlen int 31 | Version uint64 32 | } 33 | 34 | func NewKey(version uint64, c Cipher, bm CipherBlockMode, key []byte) (*Key, error) { 35 | k := &Key{ 36 | Cipher: c, 37 | CipherBlockMode: bm, 38 | Version: version, 39 | } 40 | 41 | switch c { 42 | case AES: 43 | var err error 44 | k.block, err = aes.NewCipher(key) 45 | if err != nil { 46 | return k, err 47 | } 48 | } 49 | 50 | k.ivlen = k.block.BlockSize() 51 | if k.CipherBlockMode == GCM { 52 | k.ivlen = 12 53 | } 54 | 55 | return k, nil 56 | } 57 | 58 | func (k *Key) Encrypt(dst, src []byte) ([]byte, error) { 59 | if len(dst) != len(src)+8+k.ivlen { 60 | dst = make([]byte, len(src)+8+k.ivlen) 61 | } 62 | 63 | // Place encryption KeyID at the beginning of cipher text 64 | binary.PutUvarint(dst[:8], k.Version) 65 | 66 | // Followed by the IV 67 | iv := dst[8 : k.ivlen+8] 68 | if _, err := io.ReadAtLeast(rand.Reader, iv, k.ivlen); err != nil { 69 | return src, err 70 | } 71 | 72 | switch k.CipherBlockMode { 73 | case CFB: 74 | stream := cipher.NewCFBEncrypter(k.block, iv) 75 | stream.XORKeyStream(dst[8+k.ivlen:], src) 76 | case CTR: 77 | stream := cipher.NewCTR(k.block, iv) 78 | stream.XORKeyStream(dst[8+k.ivlen:], src) 79 | case OFB: 80 | stream := cipher.NewOFB(k.block, iv) 81 | stream.XORKeyStream(dst[8+k.ivlen:], src) 82 | case GCM: 83 | aead, err := cipher.NewGCM(k.block) 84 | if err != nil { 85 | return dst, err 86 | } 87 | 88 | dst = aead.Seal(dst[:8+k.ivlen], iv, src, nil) 89 | } 90 | 91 | return dst, nil 92 | } 93 | 94 | func (k *Key) Decrypt(dst, src []byte) ([]byte, error) { 95 | if len(src) == 0 { 96 | return []byte{}, nil 97 | } 98 | 99 | if len(src) < k.block.BlockSize() { 100 | return nil, errors.New("Insufficient length") 101 | } 102 | 103 | iv := src[:k.ivlen] 104 | 105 | text := src[k.ivlen:] 106 | switch k.CipherBlockMode { 107 | case CFB: 108 | stream := cipher.NewCFBDecrypter(k.block, iv) 109 | dst = dst[:len(text)] 110 | stream.XORKeyStream(dst, text) 111 | case CTR: 112 | stream := cipher.NewCTR(k.block, iv) 113 | dst = dst[:len(text)] 114 | stream.XORKeyStream(dst, text) 115 | case OFB: 116 | stream := cipher.NewOFB(k.block, iv) 117 | dst = dst[:len(text)] 118 | stream.XORKeyStream(dst, text) 119 | case GCM: 120 | aead, err := cipher.NewGCM(k.block) 121 | if err != nil { 122 | return dst, err 123 | } 124 | return aead.Open(dst[:0], iv, text, nil) 125 | } 126 | return dst, nil 127 | } 128 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Superdog - the Crypto library for Vault from Hashicorp 2 | [![GoDoc](https://godoc.org/github.com/xordataexchange/superdog?status.svg)](http://godoc.org/github.com/xordataexchange/superdog) 3 | 4 | 5 | Superdog is a library for managing strong cryptography in both development and test environments. Superdog provides an elegant wrapper to the [Vault](https://www.vaultproject.io) API that allows you to manage your cryptographic keys using any code that implements the `KeyProvider` interface. An implemention of the `KeyProvider` interface is provided for Vault, but others could be supported. 6 | 7 | [Intro article](https://blog.gopheracademy.com/advent-2015/superdog/) 8 | 9 | ### Features 10 | 11 | - Versioned Keys - Key version is stored as the first few bytes of the encrypted text 12 | - Key Rotation - Rotate your keys safely, knowing that you'll always be able to decrypt older versionss 13 | - Development implementation for tests and local development 14 | - Versioned and Rotated IV/Salt - `SaltProvider` interface works the same as `KeyProvider` to allow development and testing access to the crypto libraries without requiring a live Key (Vault) server 15 | - `Reencrypt` function to simplify key rotation, decrypts with given key, reencrypts with latest key 16 | 17 | ### Cypher Suites 18 | 19 | `superdog` supports AES encryption with CFB/CTR/GCM/OFB modes. 20 | 21 | ### Versioning 22 | 23 | Superdog follows semver and is available under `gopkg.in` as `gopkg.in/xordataexchange/superdog.v1` 24 | 25 | ### Performance 26 | 27 | On Go version 1.5.2 / Linux x86_64 kernel 4.2.5 on a quad-core i7: 28 | 29 | ``` 30 | BenchmarkKeyEncryptCFB-8 1000000 2024 ns/op 31 | BenchmarkKeyEncryptCTR-8 500000 2748 ns/op 32 | BenchmarkKeyEncryptGCM-8 1000000 2381 ns/op 33 | BenchmarkKeyEncryptOFB-8 500000 2665 ns/op 34 | BenchmarkKeyDecryptCFB-8 10000000 215 ns/op 35 | BenchmarkKeyDecryptCTR-8 2000000 898 ns/op 36 | BenchmarkKeyDecryptGCM-8 3000000 520 ns/op 37 | BenchmarkKeyDecryptOFB-8 2000000 817 ns/op 38 | ``` 39 | 40 | ### Usage 41 | 42 | `go get -u github.com/xordataexchange/superdog/...` 43 | 44 | #### Encryption 45 | ```go 46 | val := []byte("encrypt me!") 47 | 48 | // use a key prefix to delineate different crypto keys 49 | // allowing you to use different keys for different parts of your application 50 | // or different fields of a database table, for example 51 | b, err := Encrypt("mykeyprefix", val, val) 52 | if err != nil { 53 | // handle error 54 | } 55 | ``` 56 | #### Decryption 57 | ```go 58 | b := []byte["some crypt cypher text here"] 59 | decrypted, err := Decrypt([]byte("mykeyprefix", b, b) 60 | if err != nil { 61 | // handle error 62 | } 63 | 64 | ``` 65 | 66 | #### Production Usage 67 | By default, `superdog` uses the `DevKeyProvider` which is a static key with static IV. This is extremely insecure, and SHOULD NOT ever be used in production. 68 | 69 | We reccommend using Go's [build tags](https://golang.org/pkg/go/build/) to enable strong cryptography in production usage. 70 | 71 | Create a file with your connection routines in the init() function. Add the build tag `// +build production` to the top of that file. Here's an incomplete example: 72 | 73 | ```go 74 | // +build production 75 | 76 | package main 77 | import ( 78 | "github.com/xordataexchange/superdog" 79 | "github.com/xordataexchange/superdog/vault/hashi" 80 | "github.com/hashicorp/vault/api" 81 | ) 82 | 83 | // Assign each application a unique UUID 84 | // and use Vault's AppID authentication mechanism 85 | const ( 86 | appid = "SOME RANDOM UUID" 87 | ) 88 | 89 | func init() { 90 | user := os.Getenv("VAULT_USER") 91 | vaultaddr := os.Getenv("VAULT_ADDRESS") 92 | // TEST these for empty strings & handle appropriately in your code 93 | 94 | cfg:= api.DefaultConfig() 95 | cfg.Address = vaultaddr 96 | 97 | vault, err := hashi.NewVault(cfg) 98 | if err != nil { 99 | // handle appropriately 100 | } 101 | err = vault.AuthAppID(appid, user) 102 | if err != nil { 103 | // handle appropriately 104 | } 105 | 106 | crypto.DefaultKeyProvider = vault 107 | crypto.DefaultSaltProvider = vault 108 | 109 | } 110 | 111 | ``` 112 | Now compile your program with `go build -tags production` to include this code. The `KeyProvider` will be set to use Vault. 113 | 114 | -------------------------------------------------------------------------------- /crypto.go: -------------------------------------------------------------------------------- 1 | package superdog 2 | 3 | import ( 4 | "bytes" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "encoding/binary" 8 | "errors" 9 | ) 10 | 11 | var DefaultKeyProvider KeyProvider = new(DevKeyProvider) 12 | var DefaultSaltProvider SaltProvider = new(DevSaltProvider) 13 | 14 | // Encrypt will encrypt the provided byte slice with the latesg key. It returns a new slice as it prepends the key version, and IV. 15 | func Encrypt(prefix string, dst, src []byte) ([]byte, error) { 16 | v, err := DefaultKeyProvider.CurrentKeyVersion(prefix) 17 | if err != nil { 18 | return nil, err 19 | } 20 | return EncryptWithVersion(prefix, v, dst, src) 21 | } 22 | 23 | // EncryptWithVersion will encrypt the provided byte slice with the supplied key version. It returns a new slice as it prepends the key version, and IV. 24 | func EncryptWithVersion(keyPrefix string, keyVersion uint64, dst []byte, src []byte) ([]byte, error) { 25 | if len(src) == 0 { 26 | return dst[:0], nil 27 | } 28 | 29 | k, err := DefaultKeyProvider.GetKey(keyPrefix, keyVersion) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | return k.Encrypt(dst, src) 35 | } 36 | 37 | // Decrypt will decrypt the provided byte slice using the provided key at the version it was encrypted with. It returns a new slice as it trims the prefixed key version and IV. It modifies the same underlying array. 38 | func Decrypt(keyPrefix string, dst, src []byte) ([]byte, error) { 39 | if len(src) == 0 { 40 | return []byte{}, nil 41 | } 42 | 43 | if len(src) <= 8 { 44 | return nil, errors.New("Insufficient length") 45 | } 46 | 47 | buf := bytes.NewBuffer(src) 48 | version, err := binary.ReadUvarint(buf) 49 | if err != nil { 50 | return src, err 51 | } 52 | 53 | k, err := DefaultKeyProvider.GetKey(keyPrefix, version) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | return k.Decrypt(src, src[8:]) 59 | } 60 | 61 | // Reencrypt takes encrypted ciphertext, decrypts it with the version of the key used to decrypt it, and re-encrypts the plaintext with the current version of the key. 62 | func Reencrypt(keyPrefix string, dst, src []byte) ([]byte, error) { 63 | dst, err := Decrypt(keyPrefix, dst, src) 64 | if err != nil { 65 | return src, err 66 | } 67 | v, err := DefaultKeyProvider.CurrentKeyVersion(keyPrefix) 68 | if err != nil { 69 | return nil, err 70 | } 71 | return EncryptWithVersion(keyPrefix, v, dst, dst) 72 | } 73 | 74 | // CurrentHashes returns a list of all possible hashes for the given prefix and value, used as search criteria during rotation 75 | func CurrentHashes(prefix string, value []byte) ([][]byte, error) { 76 | hashes := make([][]byte, 0) 77 | salts, err := DefaultSaltProvider.CurrentSalts(prefix) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | for _, v := range salts { 83 | h, err := HashWithVersion(prefix, v, value) 84 | if err != nil { 85 | return nil, err 86 | } 87 | hashes = append(hashes, h) 88 | } 89 | 90 | return hashes, nil 91 | } 92 | 93 | // CurrentHashesString returns a list of all possible hashes for the given prefix and value, used as search criteria during rotation 94 | func CurrentHashesString(prefix string, value string) ([]string, error) { 95 | hashes := make([]string, 0) 96 | if len(value) == 0 { 97 | return hashes, nil 98 | } 99 | 100 | salts, err := DefaultSaltProvider.CurrentSalts(prefix) 101 | if err != nil { 102 | return nil, err 103 | } 104 | 105 | for _, v := range salts { 106 | h, err := HashWithVersion(prefix, v, []byte(value)) 107 | if err != nil { 108 | return nil, err 109 | } 110 | hashes = append(hashes, string(h)) 111 | } 112 | 113 | return hashes, nil 114 | } 115 | 116 | // Hash returns a hash to be used for the given value using the current version 117 | func Hash(prefix string, value []byte) ([]byte, error) { 118 | v, err := DefaultSaltProvider.CurrentSaltVersion(prefix) 119 | if err != nil { 120 | return nil, err 121 | } 122 | return HashWithVersion(prefix, v, value) 123 | } 124 | 125 | // HashWithVersion returns a hash to be used for the given value using the supplied version 126 | func HashWithVersion(prefix string, version uint64, value []byte) ([]byte, error) { 127 | if len(value) == 0 { 128 | return []byte{}, nil 129 | } 130 | s, err := DefaultSaltProvider.GetSalt(prefix, version) 131 | if err != nil { 132 | return nil, err 133 | } 134 | 135 | hash := sha256.Sum256(append(s, value...)) 136 | i := base64.StdEncoding.EncodedLen(32) 137 | out := make([]byte, i) 138 | 139 | base64.StdEncoding.Encode(out, hash[:]) 140 | 141 | return out, nil 142 | } 143 | 144 | // HashString returns a hash to be used for the given value using the current version 145 | func HashString(prefix string, value string) (string, error) { 146 | b, err := Hash(prefix, []byte(value)) 147 | if err != nil { 148 | return "", err 149 | } 150 | return string(b), nil 151 | } 152 | -------------------------------------------------------------------------------- /key_test.go: -------------------------------------------------------------------------------- 1 | package superdog 2 | 3 | import ( 4 | "bytes" 5 | "crypto/aes" 6 | "crypto/rand" 7 | "io" 8 | "testing" 9 | ) 10 | 11 | func TestKeyEncrypt(t *testing.T) { 12 | val := "Test Value" 13 | ptext := []byte(val) 14 | k, err := NewKey(1, AES, GCM, []byte("Default Key XOR ")) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | 19 | b, err := k.Encrypt(ptext, ptext) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | if len(b) != len(val)+36 { 25 | t.Fatal("Encrypted value should be proper length") 26 | } 27 | 28 | k2, err := NewKey(1, AES, CFB, []byte("Default Key XOR ")) 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | b, err = k2.Encrypt(ptext, ptext) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | if len(b) != len(val)+8+aes.BlockSize { 38 | t.Fatal("Encrypted value should be proper length") 39 | } 40 | } 41 | 42 | func TestKeyDecrypt(t *testing.T) { 43 | val := "Test Value" 44 | ptext := []byte(val) 45 | k, err := NewKey(1, AES, GCM, []byte("Default Key XOR ")) 46 | b, err := k.Encrypt(ptext, ptext) 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | 51 | decrypted, err := k.Decrypt(b, b[8:]) 52 | if err != nil { 53 | t.Fatal("Error decrypting value", err) 54 | } 55 | 56 | if !bytes.Equal([]byte(val), decrypted) { 57 | t.Fatal("Expected decrypted value to match original value", val, string(decrypted)) 58 | } 59 | 60 | ptext = []byte(val) 61 | k2, err := NewKey(1, AES, CFB, []byte("Default Key XOR ")) 62 | b, err = k2.Encrypt(ptext, ptext) 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | 67 | decrypted, err = k2.Decrypt(b, b[8:]) 68 | if err != nil { 69 | t.Fatal("Error decrypting value", err) 70 | } 71 | 72 | if !bytes.Equal([]byte(val), decrypted) { 73 | t.Fatal("Expected decrypted value to match original value", val, string(decrypted)) 74 | } 75 | } 76 | 77 | func BenchmarkKeyEncryptCFB(b *testing.B) { 78 | val := []byte("Test Value") 79 | 80 | var key [16]byte 81 | if _, err := io.ReadFull(rand.Reader, key[:]); err != nil { 82 | b.Fatal(err) 83 | } 84 | k, err := NewKey(1, AES, CFB, key[:]) 85 | if err != nil { 86 | b.Fatal(err) 87 | } 88 | 89 | b.ResetTimer() 90 | for i := 0; i < b.N; i++ { 91 | k.Encrypt(val, val) 92 | } 93 | } 94 | 95 | func BenchmarkKeyEncryptCTR(b *testing.B) { 96 | val := []byte("Test Value") 97 | var key [16]byte 98 | if _, err := io.ReadFull(rand.Reader, key[:]); err != nil { 99 | b.Fatal(err) 100 | } 101 | k, err := NewKey(1, AES, CTR, key[:]) 102 | if err != nil { 103 | b.Fatal(err) 104 | } 105 | 106 | b.ResetTimer() 107 | for i := 0; i < b.N; i++ { 108 | k.Encrypt(val, val) 109 | } 110 | } 111 | 112 | func BenchmarkKeyEncryptGCM(b *testing.B) { 113 | val := []byte("Test Value") 114 | var key [16]byte 115 | if _, err := io.ReadFull(rand.Reader, key[:]); err != nil { 116 | b.Fatal(err) 117 | } 118 | k, err := NewKey(1, AES, GCM, key[:]) 119 | if err != nil { 120 | b.Fatal(err) 121 | } 122 | 123 | b.ResetTimer() 124 | for i := 0; i < b.N; i++ { 125 | k.Encrypt(val, val) 126 | } 127 | } 128 | 129 | func BenchmarkKeyEncryptOFB(b *testing.B) { 130 | val := []byte("Test Value") 131 | var key [16]byte 132 | if _, err := io.ReadFull(rand.Reader, key[:]); err != nil { 133 | b.Fatal(err) 134 | } 135 | k, err := NewKey(1, AES, OFB, key[:]) 136 | if err != nil { 137 | b.Fatal(err) 138 | } 139 | 140 | b.ResetTimer() 141 | for i := 0; i < b.N; i++ { 142 | k.Encrypt(val, val) 143 | } 144 | } 145 | 146 | func BenchmarkKeyDecryptCFB(b *testing.B) { 147 | val := []byte("Test Value") 148 | var key [16]byte 149 | if _, err := io.ReadFull(rand.Reader, key[:]); err != nil { 150 | b.Fatal(err) 151 | } 152 | k, err := NewKey(1, AES, CFB, key[:]) 153 | if err != nil { 154 | b.Fatal(err) 155 | } 156 | val, err = k.Encrypt(val, val) 157 | if err != nil { 158 | b.Fatal(err) 159 | } 160 | 161 | b.ResetTimer() 162 | for i := 0; i < b.N; i++ { 163 | k.Decrypt(val, val) 164 | } 165 | } 166 | 167 | func BenchmarkKeyDecryptCTR(b *testing.B) { 168 | val := []byte("Test Value") 169 | var key [16]byte 170 | if _, err := io.ReadFull(rand.Reader, key[:]); err != nil { 171 | b.Fatal(err) 172 | } 173 | k, err := NewKey(1, AES, CTR, key[:]) 174 | if err != nil { 175 | b.Fatal(err) 176 | } 177 | val, err = k.Encrypt(val, val) 178 | if err != nil { 179 | b.Fatal(err) 180 | } 181 | 182 | b.ResetTimer() 183 | for i := 0; i < b.N; i++ { 184 | k.Decrypt(val, val) 185 | } 186 | } 187 | 188 | func BenchmarkKeyDecryptGCM(b *testing.B) { 189 | val := []byte("Test Value") 190 | var key [16]byte 191 | if _, err := io.ReadFull(rand.Reader, key[:]); err != nil { 192 | b.Fatal(err) 193 | } 194 | k, err := NewKey(1, AES, GCM, key[:]) 195 | if err != nil { 196 | b.Fatal(err) 197 | } 198 | val, err = k.Encrypt(val, val) 199 | if err != nil { 200 | b.Fatal(err) 201 | } 202 | 203 | b.ResetTimer() 204 | for i := 0; i < b.N; i++ { 205 | k.Decrypt(val, val) 206 | } 207 | } 208 | 209 | func BenchmarkKeyDecryptOFB(b *testing.B) { 210 | val := []byte("Test Value") 211 | var key [16]byte 212 | if _, err := io.ReadFull(rand.Reader, key[:]); err != nil { 213 | b.Fatal(err) 214 | } 215 | k, _ := NewKey(1, AES, OFB, key[:]) 216 | val, err := k.Encrypt(val, val) 217 | if err != nil { 218 | b.Fatal(err) 219 | } 220 | 221 | b.ResetTimer() 222 | for i := 0; i < b.N; i++ { 223 | k.Decrypt(val, val) 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /crypto_test.go: -------------------------------------------------------------------------------- 1 | package superdog 2 | 3 | import ( 4 | "bytes" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "testing" 8 | ) 9 | 10 | func TestEncrypt(t *testing.T) { 11 | DefaultKeyProvider = &DevKeyProvider{DisableWarn: true} 12 | val := []byte("Test Value...") 13 | ln := len(val) + 36 14 | b, err := Encrypt("test", val, val) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | 19 | if len(b) != ln { 20 | t.Fatal("Encrypted value should be proper length") 21 | } 22 | } 23 | 24 | func TestDecrypt(t *testing.T) { 25 | DefaultKeyProvider = &DevKeyProvider{DisableWarn: true} 26 | val := []byte("Test Value") 27 | b, err := Encrypt("test", val, val) 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | 32 | decrypted, err := Decrypt("test", b, b) 33 | if err != nil { 34 | t.Fatal("Error decrypting value", err) 35 | } 36 | 37 | if !bytes.Equal(val, decrypted) { 38 | t.Fatal("Expected decrypted value to match original value", string(val), string(decrypted)) 39 | } 40 | } 41 | 42 | func TestReencrypt(t *testing.T) { 43 | DefaultKeyProvider = &DevKeyProvider{DisableWarn: true, KeyVersion: 2} 44 | original := "Test Value" 45 | val := []byte(original) 46 | b, err := EncryptWithVersion("test", 1, val, val) 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | 51 | b, err = Reencrypt("test", b, b) 52 | if err != nil { 53 | t.Fatal("Error reencrypt key.", err) 54 | } 55 | 56 | key, err := DefaultKeyProvider.GetKey("test", 2) 57 | if err != nil { 58 | t.Fatal("Error retrieving key", err) 59 | } 60 | decrypted, err := key.Decrypt(b, b[8:]) 61 | if err != nil { 62 | t.Fatal("Error decrypting value", err) 63 | } 64 | 65 | if !bytes.Equal([]byte(original), decrypted) { 66 | t.Fatal("Expected decrypted value to match original value", original, string(decrypted)) 67 | } 68 | } 69 | 70 | func TestHash(t *testing.T) { 71 | DefaultSaltProvider = &DevSaltProvider{DisableWarn: true, SaltVersion: 2} 72 | val := []byte("Test") 73 | h, err := Hash("fields/test", val) 74 | if err != nil { 75 | t.Fatal("Error hashing value", err) 76 | } 77 | 78 | expected := sha256.Sum256(append([]byte("DEV SALT fields/test 2"), val...)) 79 | if !bytes.Equal([]byte(base64.StdEncoding.EncodeToString(expected[:])), h) { 80 | t.Fatal("Value failed to hash properly") 81 | } 82 | } 83 | 84 | func TestHashString(t *testing.T) { 85 | DefaultSaltProvider = &DevSaltProvider{DisableWarn: true, SaltVersion: 3} 86 | val := "Test" 87 | h, err := HashString("fields/test", val) 88 | if err != nil { 89 | t.Fatal("Error hashing value", err) 90 | } 91 | 92 | expected := sha256.Sum256(append([]byte("DEV SALT fields/test 3"), []byte(val)...)) 93 | if base64.StdEncoding.EncodeToString(expected[:]) != h { 94 | t.Fatal("Value failed to hash properly") 95 | } 96 | } 97 | 98 | func TestHashWithVersion(t *testing.T) { 99 | DefaultSaltProvider = &DevSaltProvider{DisableWarn: true, SaltVersion: 1} 100 | val := []byte("Test") 101 | h, err := HashWithVersion("fields/test", 2, val) 102 | if err != nil { 103 | t.Fatal("Error hashing value", err) 104 | } 105 | 106 | expected := sha256.Sum256(append([]byte("DEV SALT fields/test 2"), val...)) 107 | if !bytes.Equal([]byte(base64.StdEncoding.EncodeToString(expected[:])), h) { 108 | t.Fatal("Value failed to hash properly") 109 | } 110 | } 111 | 112 | func TestCurrentHashes(t *testing.T) { 113 | DefaultSaltProvider = &DevSaltProvider{DisableWarn: true, SaltVersion: 1} 114 | val := []byte("Test") 115 | hashes, err := CurrentHashes("fields/test", val) 116 | if err != nil { 117 | t.Fatal("Error hashing value", err) 118 | } 119 | 120 | if len(hashes) != 3 { 121 | t.Fatal("Expected 3 hashes to be returned") 122 | } 123 | 124 | expected := sha256.Sum256(append([]byte("DEV SALT fields/test 1"), val...)) 125 | expected2 := sha256.Sum256(append([]byte("DEV SALT fields/test 2"), val...)) 126 | expected3 := sha256.Sum256(append([]byte("DEV SALT fields/test 3"), val...)) 127 | 128 | if !bytes.Equal([]byte(base64.StdEncoding.EncodeToString(expected[:])), hashes[0]) { 129 | t.Fatal("Value failed to hash properly") 130 | } 131 | 132 | if !bytes.Equal([]byte(base64.StdEncoding.EncodeToString(expected2[:])), hashes[1]) { 133 | t.Fatal("Value failed to hash properly") 134 | } 135 | 136 | if !bytes.Equal([]byte(base64.StdEncoding.EncodeToString(expected3[:])), hashes[2]) { 137 | t.Fatal("Value failed to hash properly") 138 | } 139 | } 140 | 141 | func TestCurrentHashesString(t *testing.T) { 142 | DefaultSaltProvider = &DevSaltProvider{DisableWarn: true, SaltVersion: 1} 143 | val := "Test" 144 | valBytes := "Test" 145 | hashes, err := CurrentHashesString("fields/test", val) 146 | if err != nil { 147 | t.Fatal("Error hashing value", err) 148 | } 149 | 150 | if len(hashes) != 3 { 151 | t.Fatal("Expected 3 hashes to be returned") 152 | } 153 | 154 | expected := sha256.Sum256(append([]byte("DEV SALT fields/test 1"), valBytes...)) 155 | expected2 := sha256.Sum256(append([]byte("DEV SALT fields/test 2"), valBytes...)) 156 | expected3 := sha256.Sum256(append([]byte("DEV SALT fields/test 3"), valBytes...)) 157 | 158 | if base64.StdEncoding.EncodeToString(expected[:]) != hashes[0] { 159 | t.Fatal("Value failed to hash properly") 160 | } 161 | 162 | if base64.StdEncoding.EncodeToString(expected2[:]) != hashes[1] { 163 | t.Fatal("Value failed to hash properly") 164 | } 165 | 166 | if base64.StdEncoding.EncodeToString(expected3[:]) != hashes[2] { 167 | t.Fatal("Value failed to hash properly") 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /vault/hashi/vault.go: -------------------------------------------------------------------------------- 1 | package hashi 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "errors" 7 | "fmt" 8 | "strconv" 9 | "strings" 10 | "sync" 11 | 12 | "github.com/xordataexchange/superdog" 13 | "github.com/xordataexchange/superdog/vault" 14 | 15 | "github.com/hashicorp/vault/api" 16 | ) 17 | 18 | var ErrVersionMismatch = errors.New("Key returned does no match requested version") 19 | 20 | var _ vault.Vault = &Vault{} 21 | 22 | type Vault struct { 23 | client *api.Client 24 | logical *api.Logical 25 | config *api.Config 26 | keyCache map[string]*superdog.Key 27 | latestKey map[string]uint64 28 | saltCache map[string][]byte 29 | currentSalts map[string][]uint64 30 | latestSalt map[string]uint64 31 | l sync.Mutex 32 | } 33 | 34 | // NewVault returns a new hashicorp Vault client 35 | func NewVault(c *api.Config) (*Vault, error) { 36 | v := Vault{ 37 | keyCache: make(map[string]*superdog.Key), 38 | latestKey: make(map[string]uint64), 39 | saltCache: make(map[string][]byte), 40 | currentSalts: make(map[string][]uint64), 41 | latestSalt: make(map[string]uint64), 42 | } 43 | client, err := api.NewClient(c) 44 | if err != nil { 45 | return &v, nil 46 | } 47 | 48 | v.config = c 49 | v.client = client 50 | v.logical = client.Logical() 51 | return &v, nil 52 | } 53 | 54 | // SetToken sets the token cookie to the new value. 55 | func (v *Vault) SetToken(t string) { 56 | v.client.SetToken(t) 57 | } 58 | 59 | // Token returns the access token currently being used. 60 | func (v *Vault) Token() string { 61 | return v.client.Token() 62 | } 63 | 64 | // ClearToken deletes the token cookie if it's set. 65 | func (v *Vault) ClearToken() { 66 | v.client.ClearToken() 67 | } 68 | 69 | // AuthAppID authenticates with Vault using an AppID and UserID and sets the access token for requests. 70 | func (v *Vault) AuthAppID(app, user string) error { 71 | resp, err := v.config.HttpClient.Post(v.config.Address+"/v1/auth/app-id/login", "application/json", strings.NewReader(fmt.Sprintf( 72 | "{\"app_id\":\"%s\", \"user_id\":\"%s\"}", app, user))) 73 | if err != nil { 74 | return err 75 | } 76 | 77 | var r api.Response 78 | r.Response = resp 79 | err = r.Error() 80 | if err != nil { 81 | return err 82 | } 83 | 84 | s, err := api.ParseSecret(r.Body) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | v.l.Lock() 90 | v.client.SetToken(s.Auth.ClientToken) 91 | v.l.Unlock() 92 | 93 | return nil 94 | } 95 | 96 | // GetKey fetches the encryption key information for the key version provided. 97 | func (v *Vault) GetKey(prefix string, version uint64) (*superdog.Key, error) { 98 | ckey := prefix + strconv.FormatUint(version, 10) 99 | v.l.Lock() 100 | defer v.l.Unlock() 101 | if k, ok := v.keyCache[ckey]; ok { 102 | return k, nil 103 | } 104 | 105 | s, err := v.logical.Read("secret/keys/" + prefix + "/" + strconv.FormatUint(version, 10)) 106 | if err != nil { 107 | return nil, err 108 | } 109 | 110 | sv, err := strconv.ParseUint(s.Data["version"].(string), 10, 64) 111 | if err != nil { 112 | return nil, err 113 | } 114 | if sv != version { 115 | return nil, ErrVersionMismatch 116 | } 117 | 118 | var cipher superdog.Cipher 119 | switch s.Data["cipher"] { 120 | case "AES": 121 | cipher = superdog.AES 122 | default: 123 | return nil, fmt.Errorf("Unsupported cipher %s", s.Data["cipher"]) 124 | } 125 | 126 | var blockMode superdog.CipherBlockMode 127 | switch s.Data["block_mode"] { 128 | case "CFB": 129 | blockMode = superdog.CFB 130 | case "CTR": 131 | blockMode = superdog.CTR 132 | case "OFB": 133 | blockMode = superdog.OFB 134 | case "GCM": 135 | blockMode = superdog.GCM 136 | default: 137 | return nil, fmt.Errorf("Unsupported cipher block mode %s", s.Data["block_mode"]) 138 | } 139 | 140 | key, err := base64.URLEncoding.DecodeString(s.Data["key"].(string)) 141 | if err != nil { 142 | return nil, err 143 | } 144 | 145 | k, err := superdog.NewKey(version, cipher, blockMode, bytes.Trim(key, "\n")) 146 | if err != nil { 147 | return nil, err 148 | } 149 | v.keyCache[ckey] = k 150 | 151 | return k, nil 152 | } 153 | 154 | // CurrentKeyVersion retrieves the latest version of the specified key to be used 155 | func (v *Vault) CurrentKeyVersion(prefix string) (uint64, error) { 156 | v.l.Lock() 157 | defer v.l.Unlock() 158 | 159 | if v, ok := v.latestKey[prefix]; ok { 160 | return v, nil 161 | } 162 | 163 | s, err := v.logical.Read("secret/keys/" + prefix + "/current") 164 | if err != nil { 165 | return 0, err 166 | } 167 | 168 | kv, err := strconv.ParseUint(s.Data["latest"].(string), 10, 64) 169 | if err != nil { 170 | return 0, fmt.Errorf("Error parsing key for %s: %s", prefix, err) 171 | } 172 | 173 | v.latestKey[prefix] = kv 174 | 175 | return v.latestKey[prefix], nil 176 | } 177 | 178 | // GetSalt fetches the salt for the prefix and version provided. 179 | func (v *Vault) GetSalt(prefix string, version uint64) ([]byte, error) { 180 | v.l.Lock() 181 | defer v.l.Unlock() 182 | ckey := prefix + strconv.FormatUint(version, 10) 183 | if s, ok := v.saltCache[ckey]; ok { 184 | return s, nil 185 | } 186 | 187 | s, err := v.logical.Read("secret/salts/" + prefix + "/" + strconv.FormatUint(version, 10)) 188 | if err != nil { 189 | return nil, err 190 | } 191 | 192 | sv, err := strconv.ParseUint(s.Data["version"].(string), 10, 64) 193 | if err != nil { 194 | return nil, err 195 | } 196 | if sv != version { 197 | return nil, ErrVersionMismatch 198 | } 199 | 200 | salt, err := base64.URLEncoding.DecodeString(s.Data["salt"].(string)) 201 | if err != nil { 202 | return nil, err 203 | } 204 | 205 | v.saltCache[ckey] = bytes.Trim(salt, "\n") 206 | 207 | return v.saltCache[ckey], nil 208 | } 209 | 210 | // CurrentSaltVersion retrieves the latest version of the specified salt to be used 211 | func (v *Vault) CurrentSaltVersion(prefix string) (uint64, error) { 212 | v.l.Lock() 213 | 214 | if version, ok := v.latestSalt[prefix]; ok { 215 | v.l.Unlock() 216 | return version, nil 217 | } 218 | 219 | v.l.Unlock() 220 | _, err := v.CurrentSalts(prefix) 221 | if err != nil { 222 | return 0, err 223 | } 224 | 225 | v.l.Lock() 226 | defer v.l.Unlock() 227 | return v.latestSalt[prefix], nil 228 | } 229 | 230 | // CurrentSalts fetches the list of currently active salts for the prefix and version provided. 231 | func (v *Vault) CurrentSalts(prefix string) ([]uint64, error) { 232 | v.l.Lock() 233 | defer v.l.Unlock() 234 | 235 | if salts, ok := v.currentSalts[prefix]; ok { 236 | return salts, nil 237 | } 238 | 239 | var salts = make([]uint64, 0) 240 | 241 | s, err := v.logical.Read("secret/salts/" + prefix + "/current") 242 | if err != nil { 243 | return nil, err 244 | } 245 | 246 | for _, s := range strings.Split(s.Data["salts"].(string), ",") { 247 | sv, err := strconv.ParseUint(s, 10, 64) 248 | if err != nil { 249 | return nil, err 250 | } 251 | 252 | salts = append(salts, sv) 253 | } 254 | 255 | sv, err := strconv.ParseUint(s.Data["latest"].(string), 10, 64) 256 | if err != nil { 257 | return nil, err 258 | } 259 | 260 | v.latestSalt[prefix] = sv 261 | v.currentSalts[prefix] = salts 262 | return salts, nil 263 | } 264 | -------------------------------------------------------------------------------- /vault/hashi/vault_test.go: -------------------------------------------------------------------------------- 1 | package hashi 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "testing" 9 | 10 | "github.com/hashicorp/vault/api" 11 | "github.com/xordataexchange/superdog" 12 | ) 13 | 14 | func TestGetKey(t *testing.T) { 15 | resp := `{ 16 | "lease_id": "secret/keys/test/1/b34fa8d3-3121-6b24-403a-e0016ec24f29", 17 | "lease_duration": 2592000, 18 | "renewable": false, 19 | "data": { 20 | "block_mode": "GCM", 21 | "cipher": "AES", 22 | "key": "REVGQVVMVCBYT1IgS0VZMQo=", 23 | "version": "1" 24 | } 25 | }` 26 | 27 | handler := func(w http.ResponseWriter, req *http.Request) { 28 | if req.RequestURI == "/v1/secret/keys/test/1" { 29 | w.Write([]byte(resp)) 30 | } 31 | } 32 | 33 | c, ln := testHTTPServer(t, http.HandlerFunc(handler)) 34 | defer ln.Close() 35 | v, err := NewVault(c) 36 | if err != nil { 37 | t.Fatal("Failed to create vault.", err) 38 | } 39 | 40 | key, err := v.GetKey("test", 1) 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | 45 | if key.Version != 1 || key.Cipher != superdog.AES || key.CipherBlockMode != superdog.GCM { 46 | t.Fatal("Key returned is invalid") 47 | } 48 | } 49 | 50 | func TestGetKeyVersionMismatch(t *testing.T) { 51 | resp := `{ 52 | "lease_id": "secret/keys/test/1/b34fa8d3-3121-6b24-403a-e0016ec24f29", 53 | "lease_duration": 2592000, 54 | "renewable": false, 55 | "data": { 56 | "block_mode": "GCM", 57 | "cipher": "AES", 58 | "key": "REVGQVVMVCBYT1IgS0VZMgo=", 59 | "version": "2" 60 | } 61 | }` 62 | 63 | handler := func(w http.ResponseWriter, req *http.Request) { 64 | if req.RequestURI == "/v1/secret/keys/test/1" { 65 | w.Write([]byte(resp)) 66 | } 67 | } 68 | 69 | c, ln := testHTTPServer(t, http.HandlerFunc(handler)) 70 | defer ln.Close() 71 | v, err := NewVault(c) 72 | if err != nil { 73 | t.Fatal("Failed to create vault.", err) 74 | } 75 | 76 | _, err = v.GetKey("test", 1) 77 | if err != ErrVersionMismatch { 78 | t.Fatal("Expected version mismatch error") 79 | } 80 | } 81 | 82 | func TestCurrentKeyVersion(t *testing.T) { 83 | resp := `{ 84 | "lease_id": "secret/keys/test/current/b34fa8d3-3121-6b24-403a-e0016ec24f29", 85 | "lease_duration": 2592000, 86 | "renewable": false, 87 | "data": { 88 | "latest": "2" 89 | } 90 | }` 91 | 92 | handler := func(w http.ResponseWriter, req *http.Request) { 93 | if req.RequestURI == "/v1/secret/keys/test/current" { 94 | w.Write([]byte(resp)) 95 | } 96 | } 97 | 98 | c, ln := testHTTPServer(t, http.HandlerFunc(handler)) 99 | defer ln.Close() 100 | v, err := NewVault(c) 101 | if err != nil { 102 | t.Fatal("Failed to create vault.", err) 103 | } 104 | 105 | version, err := v.CurrentKeyVersion("test") 106 | if err != nil { 107 | t.Fatal(err) 108 | } 109 | 110 | if version != 2 { 111 | t.Fatal("Expected current key version to be 2") 112 | } 113 | } 114 | 115 | func TestAuthAppIDValid(t *testing.T) { 116 | valid := `{"lease_id":"","renewable":false,"lease_duration":0,"data":null,"auth":{"client_token":"10e19bf2-7e1d-f9f5-f181-4a712d0754de","policies":["root"],"metadata":{"app-id":"sha1:a94a8fe5ccb19ba61c4c0873d391e987982fbbd3","user-id":"sha1:dc724af18fbdd4e59189f5fe768a5f8311527050"},"lease_duration":0,"renewable":false}}` 117 | handler := func(w http.ResponseWriter, req *http.Request) { 118 | w.Write([]byte(valid)) 119 | } 120 | 121 | c, ln := testHTTPServer(t, http.HandlerFunc(handler)) 122 | defer ln.Close() 123 | 124 | v, err := NewVault(c) 125 | if err != nil { 126 | t.Fatal("Failed to create vault.", err) 127 | } 128 | 129 | err = v.AuthAppID("test", "testing") 130 | if err != nil { 131 | t.Fatal("Failed to authenticate using AppID", err) 132 | } 133 | 134 | if v.Token() != "10e19bf2-7e1d-f9f5-f181-4a712d0754de" { 135 | t.Fatal("Failed to authenticate using AppID, invalid token returned.") 136 | } 137 | } 138 | 139 | func TestAuthAppIDInvalid(t *testing.T) { 140 | invalid := `{"errors":["invalid user ID or app ID"]}` 141 | handler := func(w http.ResponseWriter, req *http.Request) { 142 | w.WriteHeader(http.StatusBadRequest) 143 | w.Write([]byte(invalid)) 144 | } 145 | 146 | c, ln := testHTTPServer(t, http.HandlerFunc(handler)) 147 | defer ln.Close() 148 | 149 | v, err := NewVault(c) 150 | if err != nil { 151 | t.Fatal("Failed to create vault.", err) 152 | } 153 | 154 | err = v.AuthAppID("test2", "testing") 155 | if err == nil { 156 | t.Fatal("Invalid login should return error") 157 | } 158 | } 159 | 160 | func TestGetSalt(t *testing.T) { 161 | resp := `{ 162 | "lease_id": "secret/salts/ssn/1/b34fa8d3-3121-6b24-403a-e0016ec24f29", 163 | "lease_duration": 2592000, 164 | "renewable": false, 165 | "data": { 166 | "salt": "QUJDMTIzCg==", 167 | "version": "1" 168 | } 169 | }` 170 | 171 | handler := func(w http.ResponseWriter, req *http.Request) { 172 | if req.RequestURI == "/v1/secret/salts/ssn/1" { 173 | w.Write([]byte(resp)) 174 | } 175 | } 176 | 177 | c, ln := testHTTPServer(t, http.HandlerFunc(handler)) 178 | defer ln.Close() 179 | v, err := NewVault(c) 180 | if err != nil { 181 | t.Fatal("Failed to create vault.", err) 182 | } 183 | 184 | salt, err := v.GetSalt("ssn", 1) 185 | if err != nil { 186 | t.Fatal(err) 187 | } 188 | 189 | if !bytes.Equal(salt, []byte("ABC123")) { 190 | t.Fatal("Salt returned does not match.") 191 | } 192 | } 193 | 194 | func TestCurrentSalts(t *testing.T) { 195 | resp := `{ 196 | "lease_id": "secret/salts/ssn/current/b34fa8d3-3121-6b24-403a-e0016ec24f29", 197 | "lease_duration": 2592000, 198 | "renewable": false, 199 | "data": { 200 | "salts": "1,2,3", 201 | "latest": "3" 202 | } 203 | }` 204 | 205 | handler := func(w http.ResponseWriter, req *http.Request) { 206 | if req.RequestURI == "/v1/secret/salts/ssn/current" { 207 | w.Write([]byte(resp)) 208 | } 209 | } 210 | 211 | c, ln := testHTTPServer(t, http.HandlerFunc(handler)) 212 | defer ln.Close() 213 | v, err := NewVault(c) 214 | if err != nil { 215 | t.Fatal("Failed to create vault.", err) 216 | } 217 | 218 | salts, err := v.CurrentSalts("ssn") 219 | if err != nil { 220 | t.Fatal(err) 221 | } 222 | 223 | if len(salts) != 3 { 224 | t.Fatal("Expected 3 salts to be returned") 225 | } 226 | } 227 | 228 | func TestCurrentSaltVersion(t *testing.T) { 229 | resp := `{ 230 | "lease_id": "secret/salts/ssn/current/b34fa8d3-3121-6b24-403a-e0016ec24f29", 231 | "lease_duration": 2592000, 232 | "renewable": false, 233 | "data": { 234 | "salts": "1,2,3", 235 | "latest": "3" 236 | } 237 | }` 238 | 239 | handler := func(w http.ResponseWriter, req *http.Request) { 240 | if req.RequestURI == "/v1/secret/salts/ssn/current" { 241 | w.Write([]byte(resp)) 242 | } 243 | } 244 | 245 | c, ln := testHTTPServer(t, http.HandlerFunc(handler)) 246 | defer ln.Close() 247 | v, err := NewVault(c) 248 | if err != nil { 249 | t.Fatal("Failed to create vault.", err) 250 | } 251 | 252 | version, err := v.CurrentSaltVersion("ssn") 253 | if err != nil { 254 | t.Fatal(err) 255 | } 256 | 257 | if version != 3 { 258 | t.Fatal("Expected current salt version to be 3") 259 | } 260 | } 261 | 262 | func testHTTPServer( 263 | t *testing.T, handler http.Handler) (*api.Config, net.Listener) { 264 | ln, err := net.Listen("tcp", "127.0.0.1:0") 265 | if err != nil { 266 | t.Fatalf("err: %s", err) 267 | } 268 | 269 | server := &http.Server{Handler: handler} 270 | go server.Serve(ln) 271 | 272 | config := api.DefaultConfig() 273 | config.Address = fmt.Sprintf("http://%s", ln.Addr()) 274 | 275 | return config, ln 276 | } 277 | --------------------------------------------------------------------------------