mirror of
https://github.com/rancher/steve.git
synced 2025-08-31 23:20:56 +00:00
Move lasso SQL cache in Steve (#452)
* Copy pkg/cache/sql from lasso to pkg/sqlcache * Rename import from github.com/rancher/lasso/pkg/cache/sql to github.com/rancher/steve/pkg/sqlcache * Fix filter.Match -> filter.Matches * go mod tidy * Fix lint errors * Remove lasso SQL cache mentions * Fix more CI lint errors * fix goimports Signed-off-by: Silvio Moioli <silvio@moioli.net> * fix tests (Match -> Matches) Signed-off-by: Silvio Moioli <silvio@moioli.net> * Fix Sort order --------- Signed-off-by: Silvio Moioli <silvio@moioli.net> Co-authored-by: Silvio Moioli <silvio@moioli.net>
This commit is contained in:
168
pkg/sqlcache/encryption/encrypt.go
Normal file
168
pkg/sqlcache/encryption/encrypt.go
Normal file
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
Package encryption provides encryption and decryption functions, while
|
||||
abstracting away key management concerns.
|
||||
Uses AES-GCM encryption, with key rotation, keeping keys in memory.
|
||||
*/
|
||||
package encryption
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrKeyNotFound = errors.New("data key not found")
|
||||
// maxWriteCount holds the maximum amount of times the active key can be
|
||||
// used, prior to it being rotated. 2^32 is the currently recommended key
|
||||
// wear-out params by NIST for AES-GCM using random nonces.
|
||||
maxWriteCount int64 = 1 << 32
|
||||
)
|
||||
|
||||
const (
|
||||
keySize = 32 // 32 for AES-256
|
||||
)
|
||||
|
||||
// Manager uses AES-GCM encryption and keeps in memory the data encryption
|
||||
// keys. The active encryption key is automatically rotated once it has been
|
||||
// used over a certain amount of times - defined by maxWriteCount.
|
||||
type Manager struct {
|
||||
dataKeys [][]byte
|
||||
activeKeyCounter int64
|
||||
|
||||
// lock works as the mutual exclusion lock for dataKeys.
|
||||
lock sync.RWMutex
|
||||
// counterLock works as the mutual exclusion lock for activeKeyCounter.
|
||||
counterLock sync.Mutex
|
||||
}
|
||||
|
||||
// NewManager returns Manager, which satisfies db.Encryptor and db.Decryptor
|
||||
func NewManager() (*Manager, error) {
|
||||
m := &Manager{
|
||||
dataKeys: [][]byte{},
|
||||
}
|
||||
m.newDataEncryptionKey()
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Encrypt encrypts the specified data, returning: the encrypted data, the nonce used to encrypt the data, and an ID identifying the key that was used (as it rotates). On failure error is returned instead.
|
||||
func (m *Manager) Encrypt(data []byte) ([]byte, []byte, uint32, error) {
|
||||
dek, keyID, err := m.fetchActiveDataKey()
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
aead, err := createGCMCypher(dek)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
edata, nonce, err := encrypt(aead, data)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
return edata, nonce, keyID, nil
|
||||
}
|
||||
|
||||
// Decrypt accepts a chunk of encrypted data, the nonce used to encrypt it and the ID of the used key (as it rotates). It returns the decrypted data or an error.
|
||||
func (m *Manager) Decrypt(edata, nonce []byte, keyID uint32) ([]byte, error) {
|
||||
dek, err := m.key(keyID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
aead, err := createGCMCypher(dek)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create GCMCypher from DEK")
|
||||
}
|
||||
data, err := aead.Open(nil, nonce, edata, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("failed to decrypt data using keyid %d", keyID))
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func encrypt(aead cipher.AEAD, data []byte) ([]byte, []byte, error) {
|
||||
if aead == nil {
|
||||
return nil, nil, fmt.Errorf("aead is nil, cannot encrypt data")
|
||||
}
|
||||
nonce := make([]byte, aead.NonceSize())
|
||||
_, err := rand.Read(nonce)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
sealed := aead.Seal(nil, nonce, data, nil)
|
||||
return sealed, nonce, nil
|
||||
}
|
||||
|
||||
func createGCMCypher(key []byte) (cipher.AEAD, error) {
|
||||
b, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aead, err := cipher.NewGCM(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return aead, nil
|
||||
}
|
||||
|
||||
// fetchActiveDataKey returns the current data key and its key ID.
|
||||
// Each call results in activeKeyCounter being incremented by 1. When the
|
||||
// the activeKeyCounter exceeds maxWriteCount, the active data key is
|
||||
// rotated - before being returned.
|
||||
func (m *Manager) fetchActiveDataKey() ([]byte, uint32, error) {
|
||||
m.counterLock.Lock()
|
||||
defer m.counterLock.Unlock()
|
||||
|
||||
m.activeKeyCounter++
|
||||
if m.activeKeyCounter >= maxWriteCount {
|
||||
return m.newDataEncryptionKey()
|
||||
}
|
||||
|
||||
return m.activeKey()
|
||||
}
|
||||
|
||||
func (m *Manager) newDataEncryptionKey() ([]byte, uint32, error) {
|
||||
dek := make([]byte, keySize)
|
||||
_, err := rand.Read(dek)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
m.activeKeyCounter = 1
|
||||
|
||||
m.dataKeys = append(m.dataKeys, dek)
|
||||
keyID := uint32(len(m.dataKeys) - 1)
|
||||
|
||||
return dek, keyID, nil
|
||||
}
|
||||
|
||||
func (m *Manager) activeKey() ([]byte, uint32, error) {
|
||||
m.lock.RLock()
|
||||
defer m.lock.RUnlock()
|
||||
|
||||
nk := len(m.dataKeys)
|
||||
if nk == 0 {
|
||||
return nil, 0, ErrKeyNotFound
|
||||
}
|
||||
keyID := uint32(nk - 1)
|
||||
|
||||
return m.dataKeys[keyID], keyID, nil
|
||||
}
|
||||
|
||||
func (m *Manager) key(keyID uint32) ([]byte, error) {
|
||||
m.lock.RLock()
|
||||
defer m.lock.RUnlock()
|
||||
|
||||
if len(m.dataKeys) <= int(keyID) {
|
||||
return nil, fmt.Errorf("%w: %v", ErrKeyNotFound, keyID)
|
||||
}
|
||||
return m.dataKeys[keyID], nil
|
||||
}
|
327
pkg/sqlcache/encryption/encrypt_test.go
Normal file
327
pkg/sqlcache/encryption/encrypt_test.go
Normal file
@@ -0,0 +1,327 @@
|
||||
package encryption
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewManager(t *testing.T) {
|
||||
m, err := NewManager()
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.NotNil(t, m)
|
||||
}
|
||||
|
||||
func TestEncrypt(t *testing.T) {
|
||||
type testCase struct {
|
||||
description string
|
||||
test func(t *testing.T)
|
||||
}
|
||||
var tests []testCase
|
||||
|
||||
tests = append(tests, testCase{description: "test encrypt with arbitrary initial key", test: func(t *testing.T) {
|
||||
testDEK := []byte{83, 125, 203, 18, 75, 156, 24, 192, 119, 73, 157, 222, 143, 140, 231, 181, 83, 125, 203, 18, 75, 156, 24, 192, 119, 73, 157, 222, 143, 140, 231, 181}
|
||||
|
||||
m, err := NewManager()
|
||||
require.Nil(t, err)
|
||||
|
||||
m.dataKeys[0] = testDEK
|
||||
|
||||
testData := []byte("something")
|
||||
cipherText, nonce, keyID, err := m.Encrypt(testData)
|
||||
require.Nil(t, err)
|
||||
|
||||
dek := m.dataKeys[keyID]
|
||||
b, err := aes.NewCipher(dek)
|
||||
require.Nil(t, err)
|
||||
|
||||
aead, err := cipher.NewGCM(b)
|
||||
require.Nil(t, err)
|
||||
decryptedData, err := aead.Open(nil, nonce, cipherText, nil)
|
||||
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, testData, decryptedData)
|
||||
}})
|
||||
tests = append(tests, testCase{description: "test encrypt without arbitrary initial key", test: func(t *testing.T) {
|
||||
m, err := NewManager()
|
||||
require.Nil(t, err)
|
||||
|
||||
testData := []byte("something")
|
||||
cipherText, nonce, keyID, err := m.Encrypt(testData)
|
||||
require.Nil(t, err)
|
||||
|
||||
dek := m.dataKeys[keyID]
|
||||
b, err := aes.NewCipher(dek)
|
||||
require.Nil(t, err)
|
||||
|
||||
aead, err := cipher.NewGCM(b)
|
||||
require.Nil(t, err)
|
||||
decryptedData, err := aead.Open(nil, nonce, cipherText, nil)
|
||||
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, testData, decryptedData)
|
||||
}})
|
||||
tests = append(tests, testCase{description: "test encrypt: same data yield different cipher/nonce pair", test: func(t *testing.T) {
|
||||
m, err := NewManager()
|
||||
require.Nil(t, err)
|
||||
|
||||
testData := []byte("something")
|
||||
cipher1, nonce1, keyID1, err := m.Encrypt(testData)
|
||||
require.Nil(t, err)
|
||||
assert.Len(t, cipher1, 25)
|
||||
assert.Len(t, nonce1, 12)
|
||||
assert.NotEmpty(t, cipher1)
|
||||
assert.NotEmpty(t, nonce1)
|
||||
|
||||
cipher2, nonce2, keyID2, err := m.Encrypt(testData)
|
||||
require.Nil(t, err)
|
||||
|
||||
assert.Equal(t, keyID1, keyID2)
|
||||
assert.NotEqual(t, cipher1, cipher2, "each encrypt op must return a unique cipher")
|
||||
assert.NotEqual(t, nonce1, nonce2, "each encrypt op must return a unique nonce")
|
||||
}})
|
||||
tests = append(tests, testCase{description: "test encrypt with key rotation", test: func(t *testing.T) {
|
||||
m, err := NewManager()
|
||||
require.Nil(t, err)
|
||||
|
||||
testData := []byte("something")
|
||||
cipher1, nonce1, keyID1, err := m.Encrypt(testData)
|
||||
require.Nil(t, err)
|
||||
assert.Len(t, cipher1, 25)
|
||||
assert.Len(t, nonce1, 12)
|
||||
assert.NotEmpty(t, cipher1)
|
||||
assert.NotEmpty(t, nonce1)
|
||||
|
||||
m.activeKeyCounter += maxWriteCount
|
||||
|
||||
cipher2, nonce2, keyID2, err := m.Encrypt(testData)
|
||||
require.Nil(t, err)
|
||||
|
||||
assert.Equal(t, int64(1), m.activeKeyCounter)
|
||||
assert.NotEqual(t, keyID1, keyID2)
|
||||
assert.NotEqual(t, cipher1, cipher2, "each encrypt op must return a unique cipher")
|
||||
assert.NotEqual(t, nonce1, nonce2, "each encrypt op must return a unique nonce")
|
||||
}})
|
||||
t.Parallel()
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) { test.test(t) })
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecrypt(t *testing.T) {
|
||||
type testCase struct {
|
||||
description string
|
||||
test func(t *testing.T)
|
||||
}
|
||||
var tests []testCase
|
||||
|
||||
tests = append(tests, testCase{description: "test decrypt with arbitrary key", test: func(t *testing.T) {
|
||||
testDEK := []byte{83, 125, 203, 18, 75, 156, 24, 192, 119, 73, 157, 222, 143, 140, 231, 181, 83, 125, 203, 18, 75, 156, 24, 192, 119, 73, 157, 222, 143, 140, 231, 181}
|
||||
|
||||
m, err := NewManager()
|
||||
require.Nil(t, err)
|
||||
|
||||
m.dataKeys[0] = testDEK
|
||||
|
||||
testData := []byte("something")
|
||||
|
||||
// encrypt data out of band.
|
||||
b, err := aes.NewCipher(testDEK)
|
||||
require.Nil(t, err)
|
||||
|
||||
aead, err := cipher.NewGCM(b)
|
||||
require.Nil(t, err)
|
||||
|
||||
nonce := make([]byte, aead.NonceSize())
|
||||
_, err = rand.Read(nonce)
|
||||
require.Nil(t, err)
|
||||
|
||||
cipherText := aead.Seal(nil, nonce, testData, nil)
|
||||
|
||||
// use manager to decrypt the data.
|
||||
decryptedData, err := m.Decrypt(cipherText, nonce, 0)
|
||||
require.Nil(t, err)
|
||||
|
||||
assert.Equal(t, testData, decryptedData)
|
||||
},
|
||||
})
|
||||
tests = append(tests, testCase{description: "test decrypt without arbitrary key", test: func(t *testing.T) {
|
||||
m, err := NewManager()
|
||||
require.Nil(t, err)
|
||||
|
||||
testData := []byte("something")
|
||||
|
||||
// encrypt data out of band.
|
||||
dek := m.dataKeys[0]
|
||||
b, err := aes.NewCipher(dek)
|
||||
require.Nil(t, err)
|
||||
|
||||
aead, err := cipher.NewGCM(b)
|
||||
require.Nil(t, err)
|
||||
|
||||
nonce := make([]byte, aead.NonceSize())
|
||||
_, err = rand.Read(nonce)
|
||||
require.Nil(t, err)
|
||||
|
||||
cipherText := aead.Seal(nil, nonce, testData, nil)
|
||||
|
||||
// use manager to decrypt the data.
|
||||
decryptedData, err := m.Decrypt(cipherText, nonce, 0)
|
||||
require.Nil(t, err)
|
||||
|
||||
assert.Equal(t, testData, decryptedData)
|
||||
},
|
||||
})
|
||||
tests = append(tests, testCase{description: "test decrypt with wrong data nonce should return error", test: func(t *testing.T) {
|
||||
m, err := NewManager()
|
||||
require.Nil(t, err)
|
||||
|
||||
testData := []byte("something")
|
||||
|
||||
// encrypt data out of band.
|
||||
dek := m.dataKeys[0]
|
||||
b, err := aes.NewCipher(dek)
|
||||
require.Nil(t, err)
|
||||
|
||||
aead, err := cipher.NewGCM(b)
|
||||
require.Nil(t, err)
|
||||
|
||||
nonce := make([]byte, aead.NonceSize())
|
||||
_, err = rand.Read(nonce)
|
||||
require.Nil(t, err)
|
||||
|
||||
cipherText := aead.Seal(nil, nonce, testData, nil)
|
||||
|
||||
// generate random nonce.
|
||||
randomNonce := make([]byte, aead.NonceSize())
|
||||
_, err = rand.Read(nonce)
|
||||
require.Nil(t, err)
|
||||
|
||||
// decrypted encrypted data using encrypted dek
|
||||
_, err = m.Decrypt(cipherText, randomNonce, 0)
|
||||
assert.NotNil(t, err)
|
||||
},
|
||||
})
|
||||
|
||||
tests = append(tests, testCase{description: "test decrypt with DEK/nonce pair not used to encrypt should return error", test: func(t *testing.T) {
|
||||
m, err := NewManager()
|
||||
require.Nil(t, err)
|
||||
|
||||
testData := []byte("something")
|
||||
|
||||
// encrypt data out of band.
|
||||
dek := m.dataKeys[0]
|
||||
b, err := aes.NewCipher(dek)
|
||||
require.Nil(t, err)
|
||||
|
||||
aead, err := cipher.NewGCM(b)
|
||||
require.Nil(t, err)
|
||||
|
||||
nonce := make([]byte, aead.NonceSize())
|
||||
_, err = rand.Read(nonce)
|
||||
require.Nil(t, err)
|
||||
|
||||
cipherText := aead.Seal(nil, nonce, testData, nil)
|
||||
|
||||
key, id, err := m.newDataEncryptionKey()
|
||||
require.Nil(t, err)
|
||||
m.dataKeys[id] = key
|
||||
|
||||
plainText, err := m.Decrypt(cipherText, nonce, id)
|
||||
assert.NotNil(t, err)
|
||||
assert.Nil(t, plainText)
|
||||
},
|
||||
})
|
||||
tests = append(tests, testCase{description: "test decrypt for non active key", test: func(t *testing.T) {
|
||||
m, err := NewManager()
|
||||
require.Nil(t, err)
|
||||
|
||||
testData := []byte("something")
|
||||
|
||||
cipher, nonce, keyID, err := m.Encrypt(testData)
|
||||
require.Nil(t, err)
|
||||
|
||||
// force key rotation.
|
||||
m.activeKeyCounter += maxWriteCount
|
||||
_, _, newKeyID, err := m.Encrypt(nil)
|
||||
require.Nil(t, err)
|
||||
require.NotEqual(t, keyID, newKeyID)
|
||||
|
||||
// use manager to decrypt the data.
|
||||
decryptedData, err := m.Decrypt(cipher, nonce, keyID)
|
||||
require.Nil(t, err)
|
||||
|
||||
assert.Equal(t, testData, decryptedData)
|
||||
},
|
||||
})
|
||||
|
||||
t.Parallel()
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) { test.test(t) })
|
||||
}
|
||||
}
|
||||
|
||||
var buf = make([]byte, 8192)
|
||||
|
||||
func BenchmarkEncryption(b *testing.B) {
|
||||
benchEncrypt(b, 1024)
|
||||
benchEncrypt(b, 4096)
|
||||
benchEncrypt(b, 8192)
|
||||
}
|
||||
|
||||
func BenchmarkDecryption(b *testing.B) {
|
||||
benchDecrypt(b, 1024)
|
||||
benchDecrypt(b, 4096)
|
||||
benchDecrypt(b, 8192)
|
||||
}
|
||||
|
||||
func benchEncrypt(b *testing.B, size int) {
|
||||
m, err := NewManager()
|
||||
if err != nil {
|
||||
b.Fatal("failed to create manager", err)
|
||||
}
|
||||
// disable auto rotation to avoid skewing results.
|
||||
maxWriteCount = math.MaxInt32
|
||||
|
||||
b.Run(fmt.Sprintf("encrypt-%d", size), func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.SetBytes(int64(size))
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _, _, err := m.Encrypt(buf[:size])
|
||||
if err != nil {
|
||||
b.Fatal("error encrypting data", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func benchDecrypt(b *testing.B, size int) {
|
||||
m, err := NewManager()
|
||||
if err != nil {
|
||||
b.Fatal("failed to create manager", err)
|
||||
}
|
||||
|
||||
edata, enonce, kid, err := m.Encrypt(buf[:size])
|
||||
if err != nil {
|
||||
b.Fatal("failed to encrypt data", err)
|
||||
}
|
||||
|
||||
b.Run(fmt.Sprintf("decrypt-%d", size), func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.SetBytes(int64(size))
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := m.Decrypt(edata, enonce, kid)
|
||||
if err != nil {
|
||||
b.Fatal("error encrypting data", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user