diff --git a/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/envelope/BUILD b/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/envelope/BUILD new file mode 100644 index 00000000000..1cc1da54175 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/envelope/BUILD @@ -0,0 +1,30 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +load( + "@io_bazel_rules_go//go:def.bzl", + "go_library", + "go_test", +) + +go_library( + name = "go_default_library", + srcs = ["envelope.go"], + tags = ["automanaged"], + deps = [ + "//vendor/github.com/hashicorp/golang-lru:go_default_library", + "//vendor/k8s.io/apiserver/pkg/storage/value:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["envelope_test.go"], + library = ":go_default_library", + tags = ["automanaged"], + deps = [ + "//vendor/k8s.io/apiserver/pkg/storage/value:go_default_library", + "//vendor/k8s.io/apiserver/pkg/storage/value/encrypt/aes:go_default_library", + ], +) diff --git a/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/envelope/envelope.go b/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/envelope/envelope.go new file mode 100644 index 00000000000..acb26e6f5ed --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/envelope/envelope.go @@ -0,0 +1,162 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package envelope transforms values for storage at rest using a Envelope provider +package envelope + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/binary" + "fmt" + + "k8s.io/apiserver/pkg/storage/value" + + lru "github.com/hashicorp/golang-lru" +) + +// defaultCacheSize is the number of decrypted DEKs which would be cached by the transformer. +const defaultCacheSize = 1000 + +// Service allows encrypting and decrypting data using an external Key Management Service. +type Service interface { + // Decrypt a given data string to obtain the original byte data. + Decrypt(data string) ([]byte, error) + // Encrypt bytes to a string ciphertext. + Encrypt(data []byte) (string, error) +} + +type envelopeTransformer struct { + envelopeService Service + + // transformers is a thread-safe LRU cache which caches decrypted DEKs indexed by their encrypted form. + transformers *lru.Cache + + // cacheSize is the maximum number of DEKs that are cached. + cacheSize int + + // baseTransformerFunc creates a new transformer for encrypting the data with the DEK. + baseTransformerFunc func(cipher.Block) value.Transformer +} + +// NewEnvelopeTransformer returns a transformer which implements a KEK-DEK based envelope encryption scheme. +// It uses envelopeService to encrypt and decrypt DEKs. Respective DEKs (in encrypted form) are prepended to +// the data items they encrypt. A cache (of size cacheSize) is maintained to store the most recently +// used decrypted DEKs in memory. +func NewEnvelopeTransformer(envelopeService Service, cacheSize int, baseTransformerFunc func(cipher.Block) value.Transformer) (value.Transformer, error) { + if cacheSize == 0 { + cacheSize = defaultCacheSize + } + cache, err := lru.New(cacheSize) + if err != nil { + return nil, err + } + return &envelopeTransformer{ + envelopeService: envelopeService, + transformers: cache, + cacheSize: cacheSize, + baseTransformerFunc: baseTransformerFunc, + }, nil +} + +// TransformFromStorage decrypts data encrypted by this transformer using envelope encryption. +func (t *envelopeTransformer) TransformFromStorage(data []byte, context value.Context) ([]byte, bool, error) { + // Read the 16 bit length-of-DEK encoded at the start of the encrypted DEK. 16 bits can + // represent a maximum key length of 65536 bytes. We are using a 256 bit key, whose + // length cannot fit in 8 bits (1 byte). Thus, we use 16 bits (2 bytes) to store the length. + keyLen := int(binary.BigEndian.Uint16(data[:2])) + if keyLen+2 > len(data) { + return nil, false, fmt.Errorf("invalid data encountered by genvelope transformer, length longer than available bytes: %q", data) + } + encKey := string(data[2 : keyLen+2]) + encData := data[2+keyLen:] + + var transformer value.Transformer + // Look up the decrypted DEK from cache or Envelope. + _transformer, found := t.transformers.Get(encKey) + if found { + transformer = _transformer.(value.Transformer) + } else { + key, err := t.envelopeService.Decrypt(encKey) + if err != nil { + return nil, false, fmt.Errorf("error while decrypting key: %q", err) + } + transformer, err = t.addTransformer(encKey, key) + if err != nil { + return nil, false, err + } + } + return transformer.TransformFromStorage(encData, context) +} + +// TransformToStorage encrypts data to be written to disk using envelope encryption. +func (t *envelopeTransformer) TransformToStorage(data []byte, context value.Context) ([]byte, error) { + newKey, err := generateKey(32) + if err != nil { + return nil, err + } + + encKey, err := t.envelopeService.Encrypt(newKey) + if err != nil { + return nil, err + } + + transformer, err := t.addTransformer(encKey, newKey) + if err != nil { + return nil, err + } + + // Append the length of the encrypted DEK as the first 2 bytes. + encKeyLen := make([]byte, 2) + encKeyBytes := []byte(encKey) + binary.BigEndian.PutUint16(encKeyLen, uint16(len(encKeyBytes))) + + prefix := append(encKeyLen, encKeyBytes...) + + prefixedData := make([]byte, len(prefix), len(data)+len(prefix)) + copy(prefixedData, prefix) + result, err := transformer.TransformToStorage(data, context) + if err != nil { + return nil, err + } + prefixedData = append(prefixedData, result...) + return prefixedData, nil +} + +var _ value.Transformer = &envelopeTransformer{} + +// addTransformer inserts a new transformer to the Envelope cache of DEKs for future reads. +func (t *envelopeTransformer) addTransformer(encKey string, key []byte) (value.Transformer, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + transformer := t.baseTransformerFunc(block) + t.transformers.Add(encKey, transformer) + return transformer, nil +} + +// generateKey generates a random key using system randomness. +func generateKey(length int) ([]byte, error) { + key := make([]byte, length) + _, err := rand.Read(key) + if err != nil { + return nil, err + } + + return key, nil +} diff --git a/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/envelope/envelope_test.go b/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/envelope/envelope_test.go new file mode 100644 index 00000000000..0ba68afc63d --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/envelope/envelope_test.go @@ -0,0 +1,203 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package envelope + +import ( + "bytes" + "crypto/aes" + "encoding/base64" + "fmt" + "strconv" + "strings" + "testing" + + "k8s.io/apiserver/pkg/storage/value" + aestransformer "k8s.io/apiserver/pkg/storage/value/encrypt/aes" +) + +const ( + testText = "abcdefghijklmnopqrstuvwxyz" + testContextText = "0123456789" + testEnvelopeCacheSize = 10 +) + +// testEnvelopeService is a mock Envelope service which can be used to simulate remote Envelope services +// for testing of Envelope based encryption providers. +type testEnvelopeService struct { + disabled bool + keyVersion string +} + +func (t *testEnvelopeService) Decrypt(data string) ([]byte, error) { + if t.disabled { + return nil, fmt.Errorf("Envelope service was disabled") + } + dataChunks := strings.SplitN(data, ":", 2) + if len(dataChunks) != 2 { + return nil, fmt.Errorf("invalid data encountered for decryption: %s. Missing key version", data) + } + return base64.StdEncoding.DecodeString(dataChunks[1]) +} + +func (t *testEnvelopeService) Encrypt(data []byte) (string, error) { + if t.disabled { + return "", fmt.Errorf("Envelope service was disabled") + } + return t.keyVersion + ":" + base64.StdEncoding.EncodeToString(data), nil +} + +func (t *testEnvelopeService) SetDisabledStatus(status bool) { + t.disabled = status +} + +func (t *testEnvelopeService) Rotate() { + i, _ := strconv.Atoi(t.keyVersion) + t.keyVersion = strconv.FormatInt(int64(i+1), 10) +} + +func newTestEnvelopeService() *testEnvelopeService { + return &testEnvelopeService{ + keyVersion: "1", + } +} + +// Throw error if Envelope transformer tries to contact Envelope without hitting cache. +func TestEnvelopeCaching(t *testing.T) { + envelopeService := newTestEnvelopeService() + envelopeTransformer, err := NewEnvelopeTransformer(envelopeService, testEnvelopeCacheSize, aestransformer.NewCBCTransformer) + if err != nil { + t.Fatalf("failed to initialize envelope transformer: %v", err) + } + context := value.DefaultContext([]byte(testContextText)) + originalText := []byte(testText) + + transformedData, err := envelopeTransformer.TransformToStorage(originalText, context) + if err != nil { + t.Fatalf("envelopeTransformer: error while transforming data to storage: %s", err) + } + untransformedData, _, err := envelopeTransformer.TransformFromStorage(transformedData, context) + if err != nil { + t.Fatalf("could not decrypt Envelope transformer's encrypted data even once: %v", err) + } + if bytes.Compare(untransformedData, originalText) != 0 { + t.Fatalf("envelopeTransformer transformed data incorrectly. Expected: %v, got %v", originalText, untransformedData) + } + + envelopeService.SetDisabledStatus(true) + // Subsequent read for the same data should work fine due to caching. + untransformedData, _, err = envelopeTransformer.TransformFromStorage(transformedData, context) + if err != nil { + t.Fatalf("could not decrypt Envelope transformer's encrypted data using just cache: %v", err) + } + if bytes.Compare(untransformedData, originalText) != 0 { + t.Fatalf("envelopeTransformer transformed data incorrectly using cache. Expected: %v, got %v", originalText, untransformedData) + } +} + +// Makes Envelope transformer hit cache limit, throws error if it misbehaves. +func TestEnvelopeCacheLimit(t *testing.T) { + envelopeTransformer, err := NewEnvelopeTransformer(newTestEnvelopeService(), testEnvelopeCacheSize, aestransformer.NewCBCTransformer) + if err != nil { + t.Fatalf("failed to initialize envelope transformer: %v", err) + } + context := value.DefaultContext([]byte(testContextText)) + + transformedOutputs := map[int][]byte{} + + // Overwrite lots of entries in the map + for i := 0; i < 2*testEnvelopeCacheSize; i++ { + numberText := []byte(strconv.Itoa(i)) + + res, err := envelopeTransformer.TransformToStorage(numberText, context) + transformedOutputs[i] = res + if err != nil { + t.Fatalf("envelopeTransformer: error while transforming data (%v) to storage: %s", numberText, err) + } + } + + // Try reading all the data now, ensuring cache misses don't cause a concern. + for i := 0; i < 2*testEnvelopeCacheSize; i++ { + numberText := []byte(strconv.Itoa(i)) + + output, _, err := envelopeTransformer.TransformFromStorage(transformedOutputs[i], context) + if err != nil { + t.Fatalf("envelopeTransformer: error while transforming data (%v) from storage: %s", transformedOutputs[i], err) + } + + if bytes.Compare(numberText, output) != 0 { + t.Fatalf("envelopeTransformer transformed data incorrectly using cache. Expected: %v, got %v", numberText, output) + } + } +} + +func BenchmarkEnvelopeCBCRead(b *testing.B) { + envelopeTransformer, err := NewEnvelopeTransformer(newTestEnvelopeService(), testEnvelopeCacheSize, aestransformer.NewCBCTransformer) + if err != nil { + b.Fatalf("failed to initialize envelope transformer: %v", err) + } + benchmarkRead(b, envelopeTransformer, 1024) +} + +func BenchmarkAESCBCRead(b *testing.B) { + block, err := aes.NewCipher(bytes.Repeat([]byte("a"), 32)) + if err != nil { + b.Fatal(err) + } + + aesCBCTransformer := aestransformer.NewCBCTransformer(block) + benchmarkRead(b, aesCBCTransformer, 1024) +} + +func BenchmarkEnvelopeGCMRead(b *testing.B) { + envelopeTransformer, err := NewEnvelopeTransformer(newTestEnvelopeService(), testEnvelopeCacheSize, aestransformer.NewGCMTransformer) + if err != nil { + b.Fatalf("failed to initialize envelope transformer: %v", err) + } + benchmarkRead(b, envelopeTransformer, 1024) +} + +func BenchmarkAESGCMRead(b *testing.B) { + block, err := aes.NewCipher(bytes.Repeat([]byte("a"), 32)) + if err != nil { + b.Fatal(err) + } + + aesGCMTransformer := aestransformer.NewGCMTransformer(block) + benchmarkRead(b, aesGCMTransformer, 1024) +} + +func benchmarkRead(b *testing.B, transformer value.Transformer, valueLength int) { + context := value.DefaultContext([]byte(testContextText)) + v := bytes.Repeat([]byte("0123456789abcdef"), valueLength/16) + + out, err := transformer.TransformToStorage(v, context) + if err != nil { + b.Fatal(err) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + from, stale, err := transformer.TransformFromStorage(out, context) + if err != nil { + b.Fatal(err) + } + if stale { + b.Fatalf("unexpected data: %t %q", stale, from) + } + } + b.StopTimer() +}