diff --git a/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/aes/aes.go b/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/aes/aes.go index 8a180eb3c4b..daa82f711fe 100644 --- a/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/aes/aes.go +++ b/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/aes/aes.go @@ -18,9 +18,13 @@ limitations under the License. package aes import ( + "bytes" + "crypto/aes" "crypto/cipher" "crypto/rand" + "errors" "fmt" + "io" "k8s.io/apiserver/pkg/storage/value" ) @@ -78,3 +82,71 @@ func (t *gcm) TransformToStorage(data []byte, context value.Context) ([]byte, er cipherText := aead.Seal(result[nonceSize:nonceSize], result[:nonceSize], data, context.AuthenticatedData()) return result[:nonceSize+len(cipherText)], nil } + +// cbc implements encryption at rest of the provided values given a cipher.Block algorithm. +type cbc struct { + block cipher.Block +} + +// NewCBCTransformer takes the given block cipher and performs encryption and decryption on the given +// data. +func NewCBCTransformer(block cipher.Block) value.Transformer { + return &cbc{block: block} +} + +var ( + errInvalidBlockSize = fmt.Errorf("the stored data is not a multiple of the block size") + errInvalidPKCS7Data = errors.New("invalid PKCS7 data (empty or not padded)") + errInvalidPKCS7Padding = errors.New("invalid padding on input") +) + +func (t *cbc) TransformFromStorage(data []byte, context value.Context) ([]byte, bool, error) { + blockSize := aes.BlockSize + if len(data) < blockSize { + return nil, false, fmt.Errorf("the stored data was shorter than the required size") + } + iv := data[:blockSize] + data = data[blockSize:] + + if len(data)%blockSize != 0 { + return nil, false, errInvalidBlockSize + } + + result := make([]byte, len(data)) + copy(result, data) + mode := cipher.NewCBCDecrypter(t.block, iv) + mode.CryptBlocks(result, result) + + // remove and verify PKCS#7 padding for CBC + c := result[len(result)-1] + paddingSize := int(c) + size := len(result) - paddingSize + if paddingSize == 0 || paddingSize > len(result) { + return nil, false, errInvalidPKCS7Data + } + for i := 0; i < paddingSize; i++ { + if result[size+i] != c { + return nil, false, errInvalidPKCS7Padding + } + } + + return result[:size], false, nil +} + +func (t *cbc) TransformToStorage(data []byte, context value.Context) ([]byte, error) { + blockSize := aes.BlockSize + paddingSize := blockSize - (len(data) % blockSize) + result := make([]byte, blockSize+len(data)+paddingSize) + iv := result[:blockSize] + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + return nil, fmt.Errorf("unable to read sufficient random bytes") + } + copy(result[blockSize:], data) + + // add PKCS#7 padding for CBC + copy(result[blockSize+len(data):], bytes.Repeat([]byte{byte(paddingSize)}, paddingSize)) + + mode := cipher.NewCBCEncrypter(t.block, iv) + mode.CryptBlocks(result[blockSize:], result[blockSize:]) + return result, nil +} diff --git a/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/aes/aes_test.go b/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/aes/aes_test.go index ea2ea3936f1..167d6d8a984 100644 --- a/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/aes/aes_test.go +++ b/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/aes/aes_test.go @@ -20,7 +20,11 @@ import ( "bytes" "crypto/aes" "crypto/cipher" + "crypto/rand" + "encoding/hex" "fmt" + "io" + "reflect" "testing" "k8s.io/apiserver/pkg/storage/value" @@ -93,6 +97,58 @@ func TestGCMKeyRotation(t *testing.T) { } } +func TestCBCKeyRotation(t *testing.T) { + testErr := fmt.Errorf("test error") + block1, err := aes.NewCipher([]byte("abcdefghijklmnop")) + if err != nil { + t.Fatal(err) + } + block2, err := aes.NewCipher([]byte("0123456789abcdef")) + if err != nil { + t.Fatal(err) + } + + context := value.DefaultContext([]byte("authenticated_data")) + + p := value.NewPrefixTransformers(testErr, + value.PrefixTransformer{Prefix: []byte("first:"), Transformer: NewCBCTransformer(block1)}, + value.PrefixTransformer{Prefix: []byte("second:"), Transformer: NewCBCTransformer(block2)}, + ) + out, err := p.TransformToStorage([]byte("firstvalue"), context) + if err != nil { + t.Fatal(err) + } + if !bytes.HasPrefix(out, []byte("first:")) { + t.Fatalf("unexpected prefix: %q", out) + } + from, stale, err := p.TransformFromStorage(out, context) + if err != nil { + t.Fatal(err) + } + if stale || !bytes.Equal([]byte("firstvalue"), from) { + t.Fatalf("unexpected data: %t %q", stale, from) + } + + // verify changing the context fails storage + from, stale, err = p.TransformFromStorage(out, value.DefaultContext([]byte("incorrect_context"))) + if err != nil { + t.Fatalf("CBC mode does not support authentication: %v", err) + } + + // reverse the order, use the second key + p = value.NewPrefixTransformers(testErr, + value.PrefixTransformer{Prefix: []byte("second:"), Transformer: NewCBCTransformer(block2)}, + value.PrefixTransformer{Prefix: []byte("first:"), Transformer: NewCBCTransformer(block1)}, + ) + from, stale, err = p.TransformFromStorage(out, context) + if err != nil { + t.Fatal(err) + } + if !stale || !bytes.Equal([]byte("firstvalue"), from) { + t.Fatalf("unexpected data: %t %q", stale, from) + } +} + func BenchmarkGCMRead_16_1024(b *testing.B) { benchmarkGCMRead(b, 16, 1024, false) } func BenchmarkGCMRead_32_1024(b *testing.B) { benchmarkGCMRead(b, 32, 1024, false) } func BenchmarkGCMRead_32_16384(b *testing.B) { benchmarkGCMRead(b, 32, 16384, false) } @@ -170,3 +226,147 @@ func benchmarkGCMWrite(b *testing.B, keyLength int, valueLength int) { } b.StopTimer() } + +func BenchmarkCBCRead_32_1024(b *testing.B) { benchmarkCBCRead(b, 32, 1024, false) } +func BenchmarkCBCRead_32_16384(b *testing.B) { benchmarkCBCRead(b, 32, 16384, false) } +func BenchmarkCBCRead_32_16384_Stale(b *testing.B) { benchmarkCBCRead(b, 32, 16384, true) } + +func BenchmarkCBCWrite_32_1024(b *testing.B) { benchmarkCBCWrite(b, 32, 1024) } +func BenchmarkCBCWrite_32_16384(b *testing.B) { benchmarkCBCWrite(b, 32, 16384) } + +func benchmarkCBCRead(b *testing.B, keyLength int, valueLength int, stale bool) { + block1, err := aes.NewCipher(bytes.Repeat([]byte("a"), keyLength)) + if err != nil { + b.Fatal(err) + } + block2, err := aes.NewCipher(bytes.Repeat([]byte("b"), keyLength)) + if err != nil { + b.Fatal(err) + } + p := value.NewPrefixTransformers(nil, + value.PrefixTransformer{Prefix: []byte("first:"), Transformer: NewCBCTransformer(block1)}, + value.PrefixTransformer{Prefix: []byte("second:"), Transformer: NewCBCTransformer(block2)}, + ) + + context := value.DefaultContext([]byte("authenticated_data")) + v := bytes.Repeat([]byte("0123456789abcdef"), valueLength/16) + + out, err := p.TransformToStorage(v, context) + if err != nil { + b.Fatal(err) + } + // reverse the key order if stale + if stale { + p = value.NewPrefixTransformers(nil, + value.PrefixTransformer{Prefix: []byte("first:"), Transformer: NewCBCTransformer(block1)}, + value.PrefixTransformer{Prefix: []byte("second:"), Transformer: NewCBCTransformer(block2)}, + ) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + from, stale, err := p.TransformFromStorage(out, context) + if err != nil { + b.Fatal(err) + } + if stale { + b.Fatalf("unexpected data: %t %q", stale, from) + } + } + b.StopTimer() +} + +func benchmarkCBCWrite(b *testing.B, keyLength int, valueLength int) { + block1, err := aes.NewCipher(bytes.Repeat([]byte("a"), keyLength)) + if err != nil { + b.Fatal(err) + } + block2, err := aes.NewCipher(bytes.Repeat([]byte("b"), keyLength)) + if err != nil { + b.Fatal(err) + } + p := value.NewPrefixTransformers(nil, + value.PrefixTransformer{Prefix: []byte("first:"), Transformer: NewCBCTransformer(block1)}, + value.PrefixTransformer{Prefix: []byte("second:"), Transformer: NewCBCTransformer(block2)}, + ) + + context := value.DefaultContext([]byte("authenticated_data")) + v := bytes.Repeat([]byte("0123456789abcdef"), valueLength/16) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := p.TransformToStorage(v, context) + if err != nil { + b.Fatal(err) + } + } + b.StopTimer() +} + +func TestRoundTrip(t *testing.T) { + lengths := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 128, 1024} + + aes16block, err := aes.NewCipher([]byte(bytes.Repeat([]byte("a"), 16))) + if err != nil { + t.Fatal(err) + } + aes24block, err := aes.NewCipher([]byte(bytes.Repeat([]byte("b"), 24))) + if err != nil { + t.Fatal(err) + } + aes32block, err := aes.NewCipher([]byte(bytes.Repeat([]byte("c"), 32))) + if err != nil { + t.Fatal(err) + } + + tests := []struct { + name string + context value.Context + t value.Transformer + }{ + {name: "GCM 16 byte key", t: NewGCMTransformer(aes16block)}, + {name: "GCM 24 byte key", t: NewGCMTransformer(aes24block)}, + {name: "GCM 32 byte key", t: NewGCMTransformer(aes32block)}, + {name: "CBC 32 byte key", t: NewCBCTransformer(aes32block)}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + context := tt.context + if context == nil { + context = value.DefaultContext("") + } + for _, l := range lengths { + data := make([]byte, l) + if _, err := io.ReadFull(rand.Reader, data); err != nil { + t.Fatalf("unable to read sufficient random bytes: %v", err) + } + original := append([]byte{}, data...) + + ciphertext, err := tt.t.TransformToStorage(data, context) + if err != nil { + t.Errorf("TransformToStorage error = %v", err) + continue + } + + result, stale, err := tt.t.TransformFromStorage(ciphertext, context) + if err != nil { + t.Errorf("TransformFromStorage error = %v", err) + continue + } + if stale { + t.Errorf("unexpected stale output") + continue + } + + switch { + case l == 0: + if len(result) != 0 { + t.Errorf("Round trip failed len=%d\noriginal:\n%s\nresult:\n%s", l, hex.Dump(original), hex.Dump(result)) + } + case !reflect.DeepEqual(original, result): + t.Errorf("Round trip failed len=%d\noriginal:\n%s\nresult:\n%s", l, hex.Dump(original), hex.Dump(result)) + } + } + }) + } +}