Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change vault to use GCM with counter #178

Merged
merged 12 commits into from Mar 6, 2019
28 changes: 28 additions & 0 deletions security/testdata/ca.key
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDVCwxvazrjIbap
k4fP0gFThXS1xaWNucwp47w0xMCH0mOi5B1JGfHlRAJmfQOn4jqzEm+2nTzwQmR/
EN0wV5MPWg1cLTiSpDrFN9nmkJZiZiN5NROJpUPw4sozIkSBli055TSaNErf2KHF
1lylR4ZyMV313zS+hb5wPfzbSPrmey6f8eY6b1FoS/5caTGweoxk764qRm6UU25Z
cfGaTfpI3KsgN+Vot9qjSfdI7dDs9jRCf7BYCPhm0DfH9e3JBC/HyRQrbxghpjqQ
8Van3a8OIWeV/AV1BI2gTrzeCWIAhatNAfjRaEVIpTU+1KJc+bGlxUKldqviGICC
YKFnxLSTAgMBAAECggEAHS9gSrsz2/24Wk69ojiwudJkhKpI3buAPpTWKZxyi6jE
wYHiiSsmujOw6H1jzNHvHKz/5NJxkLBnuAiFZKP6n3XEssX3JA+fhXj7Pty73UsE
vQwKWybqwcsvzAV7wQzjsTS3GhDj2PqCXunY00OTJX2h05b6UMddqV60jw3WYVBq
qQ00MuZWV/rwF4Gmt28PCSnMxooLvNB6pJlH0q64prwiJoe/oEDd8yFDAIaOEDw9
5E6aPnPfCkj4pu3fraHdcGYCyLGsidS+EZ/hPxD1XL/Z0K/SU0o06jw/WV056Op2
ejcptcMXRXKdWy9+blUk1nniwBAE/dEbyssKulADAQKBgQD1KCxeq02Rr8i0cMQw
XaVYgCHzqDb6NmrE7AHAq/ouO3oGVH4a1r/ApVcp2/h8DZYWFuw7E2EoSCS9QrBk
2UB6h2zF471LE99JH9wPLhWD7LOsuAiunNzpgkysNbmBW7bJQvWiCoBja3gyJyMm
F587vEsKkFRQxh1/ElWfTK0MwQKBgQDed0MgkCSfNKB8NO7oUHGsyZPKH8b2tX+E
Yql+F19HXOrb7FlLgte0I7lB05CaT3wARZEarVsucK0bDt2rSB1E11SEJtVLYrZ2
/dSVk9aHAvDhAOtTj+KEVwDSgfj8spkt+0nCtIUUbsW1/vnO6OwxCF26G8jZNBnB
SJzX3cQSUwKBgCZcfezmY0Hrvr01dA2Zabkae7WT2d53S2e7Al8yyfgYCHUbHYx3
lBPCC4yaRhyrR5P3TEnGM4rJFy6iU9XEBQnnTQb+Ju2rk2Hu4VFixa0aCdd6CKnC
E/NaF0NPONLcFhMSLjuH5yUneOxoIWDhi2IeiaOCiB8HkTAEH2/I4L9BAoGBAL0I
ijm5QeUmSthAAmHVOUKhZrtxlRc90kUjsPI72fJBui91/cp0O+YOFPUiWNVGhQ+W
DV6lv70OcYl0cFeCx5wffOluNgAAuRsTRPh0zu2aSiRnK4+ty8S4STKWzoOrHw47
YMnZqttZ5RZousxej5R6j2n9AgXOh7P9h4jGID2RAoGAPCvBqiJco4udQsosuyt/
Z0dOjF6sSaiAnx7W2+0aKMRZOhtChlCq0SFvqTvKB7X9UvEVFSyfGEBOawiNXWDu
PHeDB1DWcqToDTKqS5V4dqS+3ZaMHjvX39YhnjLCS0c3nofVWW+Pay11cC9i8tV1
OILObT2tga2txO4JxUrs+NU=
-----END PRIVATE KEY-----
1 change: 1 addition & 0 deletions security/testdata/gaia_vault
@@ -0,0 +1 @@
0xn40kNax9KkbKhUZZGYmIUZJaFDQ1ajEkyB1OgnRf6MSUhe2RK8xqlVj9mBcTMVin3_UPBlXMCrrDt5fbw0kw==
86 changes: 71 additions & 15 deletions security/vault.go
Expand Up @@ -4,21 +4,24 @@ import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"sync"

"github.com/gaia-pipeline/gaia"
)

const (
vaultName = ".gaia_vault"
keySize = 32
secretCheckKey = "GAIA_CHECK_SECRET"
secretCheckValue = "!CHECK_ME!"
)
Expand Down Expand Up @@ -57,6 +60,8 @@ type Vault struct {
cert []byte
data map[string][]byte
sync.RWMutex
counter uint64
key []byte
}

// NewVault creates a vault which is a simple k/v storage medium with AES encryption.
Expand All @@ -82,8 +87,15 @@ func NewVault(ca CAAPI, storer VaultStorer) (*Vault, error) {
if err != nil {
return nil, err
}
if len(data) < 32 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we can use keySize here?

return nil, errors.New("key lenght should be longer than 32")
}
h := sha256.New()
h.Write(data)
sum := h.Sum(nil)
v.storer = storer
v.cert = data
v.key = sum[:keySize]
v.data = make(map[string][]byte, 0)
return v, nil
}
Expand Down Expand Up @@ -192,29 +204,73 @@ func (v *Vault) encrypt(data []byte) (string, error) {
// User has deleted all the secrets. the file will be empty.
return "", nil
}
secretCheck := fmt.Sprintf("\n%s=%s", secretCheckKey, secretCheckValue)
data = append(data, []byte(secretCheck)...)
paddedPassword := v.pad(v.cert)
ci := base64.URLEncoding.EncodeToString(paddedPassword)
block, err := aes.NewCipher([]byte(ci[:aes.BlockSize]))
key := v.key
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}

msg := v.pad(data)
ciphertext := make([]byte, aes.BlockSize+len(msg))
iv := ciphertext[:aes.BlockSize]
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
v.counter++
nonce := make([]byte, 12)
binary.LittleEndian.PutUint64(nonce, v.counter)
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}

cfb := cipher.NewCFBEncrypter(block, iv)
cfb.XORKeyStream(ciphertext[aes.BlockSize:], []byte(msg))
finalMsg := base64.URLEncoding.EncodeToString(ciphertext)
ciphertext := aesgcm.Seal(nil, nonce, data, nil)
hexNonce := hex.EncodeToString(nonce)
hexChiperText := hex.EncodeToString(ciphertext)
content := fmt.Sprintf("%s||%s", hexNonce, hexChiperText)
finalMsg := hex.EncodeToString([]byte(content))
return finalMsg, nil
}

func (v *Vault) decrypt(data []byte) ([]byte, error) {
func (v *Vault) decrypt(encodedData []byte) ([]byte, error) {
if len(encodedData) < 1 {
gaia.Cfg.Logger.Info("the vault is empty")
return []byte{}, nil
}
key := v.key
decodedMsg, err := hex.DecodeString(string(encodedData))
if err != nil {
if msg, err := v.legacyDecrypt(encodedData); err == nil {
return msg, nil
}
return []byte{}, err
}
split := strings.Split(string(decodedMsg), "||")
if len(split) < 2 {
message := fmt.Sprintln("invalid number of returned splits from data. was: ", len(split))
return []byte{}, errors.New(message)
}
nonce, err := hex.DecodeString(split[0])
if err != nil {
return []byte{}, err
}
data, err := hex.DecodeString(split[1])
if err != nil {
return []byte{}, err
}
v.counter = binary.LittleEndian.Uint64(nonce)
block, err := aes.NewCipher(key)
if err != nil {
return []byte{}, err
}

aesgcm, err := cipher.NewGCM(block)
if err != nil {
return []byte{}, err
}

plaintext, err := aesgcm.Open(nil, nonce, []byte(data), nil)
if err != nil {
return []byte{}, err
}
return plaintext, nil
}

func (v *Vault) legacyDecrypt(data []byte) ([]byte, error) {
if len(data) < 1 {
gaia.Cfg.Logger.Info("the vault is empty")
return []byte{}, nil
Expand Down
133 changes: 128 additions & 5 deletions security/vault_test.go
Expand Up @@ -2,10 +2,12 @@ package security

import (
"bytes"
"encoding/hex"
"errors"
"io/ioutil"
"os"
"reflect"
"strings"
"testing"

"github.com/gaia-pipeline/gaia"
Expand Down Expand Up @@ -130,20 +132,20 @@ func TestCloseLoadSecretsWithInvalidPassword(t *testing.T) {
}
mvs := new(MockVaultStorer)
v.storer = mvs
v.cert = []byte("test")
v.key = []byte("change this password to a secret")
v.Add("key1", []byte("value1"))
v.Add("key2", []byte("value2"))
err = v.SaveSecrets()
if err != nil {
t.Fatal(err)
}
v.data = make(map[string][]byte, 0)
v.cert = []byte("tset")
v.key = []byte("change this pa00word to a secret")
err = v.LoadSecrets()
if err == nil {
t.Fatal("error should not have been nil.")
}
expected := "possible mistyped password"
expected := "cipher: message authentication failed"
if err.Error() != expected {
t.Fatalf("didn't get the right error. expected: \n'%s'\n error was: \n'%s'\n", expected, err.Error())
}
Expand All @@ -170,12 +172,12 @@ func TestAnExistingVaultFileIsNotOverwritten(t *testing.T) {
defer os.Remove(vaultName)
defer os.Remove("ca.crt")
defer os.Remove("ca.key")
v.cert = []byte("test")
v.key = []byte("change this password to a secret")
v.Add("test", []byte("value"))
v.SaveSecrets()
v2, _ := NewVault(c, nil)
v2.storer = mvs
v2.cert = []byte("test")
v2.key = []byte("change this password to a secret")
v2.LoadSecrets()
if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -366,3 +368,124 @@ func TestDefaultStorerIsAFileStorer(t *testing.T) {
t.Fatal("default filestorer not created when nil is passed in")
}
}

func TestNonceCounter(t *testing.T) {
tmp, _ := ioutil.TempDir("", "TestNonceCounter")
gaia.Cfg = &gaia.Config{}
gaia.Cfg.VaultPath = tmp
gaia.Cfg.CAPath = tmp
buf := new(bytes.Buffer)
gaia.Cfg.Logger = hclog.New(&hclog.LoggerOptions{
Level: hclog.Trace,
Output: buf,
Name: "Gaia",
})
c, _ := InitCA()
v, err := NewVault(c, nil)
if err != nil {
t.Fatal(err)
}
mvs := new(MockVaultStorer)
v.storer = mvs
v.Add("key1", []byte("value1"))
beginCounter := v.counter
for i := 0; i < 3; i++ {
err = v.SaveSecrets()
if err != nil {
t.Fatal(err)
}
err = v.LoadSecrets()
if err != nil {
t.Fatal(err)
}
}
if v.counter == beginCounter {
t.Fatal("counter should have not equaled to the count at the begin of the test.")
}
want := uint64(3)
if v.counter != want {
t.Fatalf("counter should have been %d. got: %d\n", want, v.counter)
}
}

func TestEmptyVault(t *testing.T) {
buf := new(bytes.Buffer)
gaia.Cfg.Logger = hclog.New(&hclog.LoggerOptions{
Level: hclog.Trace,
Output: buf,
Name: "Gaia",
})
v := Vault{}
t.Run("empty vault", func(t *testing.T) {
data := []byte{}
_, err := v.decrypt(data)
if err != nil {
t.Fatal("was not expecting an error. was: ", err)
}
want := "the vault is empty"
if strings.Contains(want, buf.String()) {
t.Fatalf("wanted log message '%s'. Got: %s", want, buf.String())
}
})
}

func TestAllTheHexDecrypts(t *testing.T) {
v := Vault{}
t.Run("encoded data", func(t *testing.T) {
data := []byte("invalid")
_, err := v.decrypt(data)
if err == nil {
t.Fatal("should have failed since data is not valid hex string")
}
})
t.Run("invalid data format", func(t *testing.T) {
d := []byte("asdf&&asdf")
data := []byte(hex.EncodeToString(d))
_, err := v.decrypt(data)
if err == nil {
t.Fatal("should have failed since data did not contain delimiter")
}
want := "invalid number of returned splits from data. was: 1\n"
if err.Error() != want {
t.Fatalf("want: %s, got: %s", want, err.Error())
}
})
t.Run("invalid nonce", func(t *testing.T) {
d := []byte("asdf||asdf")
data := []byte(hex.EncodeToString(d))
_, err := v.decrypt(data)
if err == nil {
t.Fatal("should have failed since data did not contain delimiter")
}
})
t.Run("invalid data", func(t *testing.T) {
nonce := hex.EncodeToString([]byte("valid"))
d := []byte(nonce + "||asdf")
data := []byte(hex.EncodeToString(d))
_, err := v.decrypt(data)
if err == nil {
t.Fatal("should have failed since data did not contain delimiter")
}
})
}

func TestLegacyDecryptOfOldVaultFile(t *testing.T) {
oldVault, err := ioutil.ReadFile("./testdata/gaia_vault")
if err != nil {
t.Fatal(err)
}
key, err := ioutil.ReadFile("./testdata/ca.key")
if err != nil {
t.Fatal(err)
}
v := Vault{
cert: key,
}
content, err := v.legacyDecrypt(oldVault)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(content), "test=secret") {
t.Fatal("was expecting content to have 'test=secret'. it was: ", string(content))
}
}