diff --git a/staging/src/k8s.io/kms/encryption/service.go b/staging/src/k8s.io/kms/encryption/service.go new file mode 100644 index 00000000000..89bc3d7dc0a --- /dev/null +++ b/staging/src/k8s.io/kms/encryption/service.go @@ -0,0 +1,262 @@ +/* +Copyright 2023 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 encryption + +import ( + "context" + "crypto/aes" + "crypto/rand" + "encoding/base64" + "fmt" + "strings" + "sync" + "time" + + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/klog/v2" + aestransformer "k8s.io/kms/pkg/encrypt/aes" + "k8s.io/kms/pkg/value" + "k8s.io/kms/service" + "k8s.io/utils/lru" +) + +var ( + // emptyContext is an empty slice of bytes. This is passed as value.Context to the + // GCM transformer. The grpc interface does not provide any additional authenticated data + // to use with AEAD. + emptyContext = value.DefaultContext([]byte{}) + // errInvalidKMSAnnotationKeySuffix is returned when the annotation key suffix is not allowed. + errInvalidKMSAnnotationKeySuffix = fmt.Errorf("annotation keys are not allowed to use %s", referenceSuffix) + + // these are var instead of const so that we can set them during tests + localKEKGenerationPollInterval = 1 * time.Second + localKEKGenerationPollTimeout = 5 * time.Minute +) + +const ( + referenceSuffix = ".reference.encryption.k8s.io" + // referenceKEKAnnotationKey is the key used to store the localKEK in the annotations. + referenceKEKAnnotationKey = "encrypted-kek" + referenceSuffix + numAnnotations = 1 + cacheSize = 1_000 + // keyLength is the length of the local KEK in bytes. + // This is the same length used for the DEKs generated in kube-apiserver. + keyLength = 32 +) + +var _ service.Service = &LocalKEKService{} + +// LocalKEKService adds an additional KEK layer to reduce calls to the remote +// KMS. +// The local KEK is generated once and stored in the LocalKEKService. This KEK +// is used for all encryption operations. For the decrypt operation, if the encrypted +// local KEK is not found in the cache, the remote KMS is used to decrypt the local KEK. +type LocalKEKService struct { + // remoteKMS is the remote kms that is used to encrypt and decrypt the local KEKs. + remoteKMS service.Service + remoteOnce sync.Once + + // transformers is a thread-safe LRU cache which caches decrypted DEKs indexed by their encrypted form. + transformers *lru.Cache + + remoteKMSResponse *service.EncryptResponse + localTransformer value.Transformer + localTransformerErr error +} + +// NewLocalKEKService is being initialized with a remote KMS service. +// In the current implementation, the localKEK Service needs to be +// restarted by the caller after security thresholds are met. +// TODO(aramase): handle rotation of local KEKs +// - when the keyID in Status() no longer matches the keyID used during encryption +// - when the local KEK has been used for a certain number of times +func NewLocalKEKService(remoteService service.Service) *LocalKEKService { + return &LocalKEKService{ + remoteKMS: remoteService, + transformers: lru.New(cacheSize), + } +} + +func (m *LocalKEKService) getTransformerForEncryption(uid string) (value.Transformer, *service.EncryptResponse, error) { + // Check if we have a local KEK + // - If exists, use the local KEK for encryption and return + // - Not exists, generate local KEK, encrypt with remote KEK, + // store it in cache encrypt the data and return. This can be + // expensive but only 1 in N calls will incur this additional latency, + // N being number of times local KEK is reused) + m.remoteOnce.Do(func() { + m.localTransformerErr = wait.PollImmediateWithContext(context.Background(), localKEKGenerationPollInterval, localKEKGenerationPollTimeout, + func(ctx context.Context) (done bool, err error) { + key, err := generateKey(keyLength) + if err != nil { + return false, fmt.Errorf("failed to generate local KEK: %w", err) + } + block, err := aes.NewCipher(key) + if err != nil { + return false, fmt.Errorf("failed to create cipher block: %w", err) + } + transformer := aestransformer.NewGCMTransformer(block) + + resp, err := m.remoteKMS.Encrypt(ctx, uid, key) + if err != nil { + klog.ErrorS(err, "failed to encrypt local KEK with remote KMS", "uid", uid) + return false, nil + } + if err = validateRemoteKMSResponse(resp); err != nil { + return false, fmt.Errorf("response annotations failed validation: %w", err) + } + m.remoteKMSResponse = copyResponseAndAddLocalKEKAnnotation(resp) + m.localTransformer = transformer + m.transformers.Add(base64.StdEncoding.EncodeToString(resp.Ciphertext), transformer) + return true, nil + }) + }) + return m.localTransformer, m.remoteKMSResponse, m.localTransformerErr +} + +func copyResponseAndAddLocalKEKAnnotation(resp *service.EncryptResponse) *service.EncryptResponse { + annotations := make(map[string][]byte, len(resp.Annotations)+numAnnotations) + for s, bytes := range resp.Annotations { + s := s + bytes := bytes + annotations[s] = bytes + } + annotations[referenceKEKAnnotationKey] = resp.Ciphertext + + return &service.EncryptResponse{ + // Ciphertext is not set on purpose - it is different per Encrypt call + KeyID: resp.KeyID, + Annotations: annotations, + } +} + +// Encrypt encrypts the plaintext with the localKEK. +func (m *LocalKEKService) Encrypt(ctx context.Context, uid string, pt []byte) (*service.EncryptResponse, error) { + transformer, resp, err := m.getTransformerForEncryption(uid) + if err != nil { + klog.V(2).InfoS("encrypt plaintext", "uid", uid, "err", err) + return nil, err + } + + ct, err := transformer.TransformToStorage(ctx, pt, emptyContext) + if err != nil { + klog.V(2).InfoS("encrypt plaintext", "uid", uid, "err", err) + return nil, err + } + + return &service.EncryptResponse{ + Ciphertext: ct, + KeyID: resp.KeyID, // TODO what about rotation ?? + Annotations: resp.Annotations, + }, nil +} + +func (m *LocalKEKService) getTransformerForDecryption(ctx context.Context, uid string, req *service.DecryptRequest) (value.Transformer, error) { + encKEK := req.Annotations[referenceKEKAnnotationKey] + + if _transformer, found := m.transformers.Get(base64.StdEncoding.EncodeToString(encKEK)); found { + return _transformer.(value.Transformer), nil + } + + key, err := m.remoteKMS.Decrypt(ctx, uid, &service.DecryptRequest{ + Ciphertext: encKEK, + KeyID: req.KeyID, + Annotations: annotationsWithoutReferenceKeys(req.Annotations), + }) + if err != nil { + return nil, err + } + + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + transformer := aestransformer.NewGCMTransformer(block) + + // Overwrite the plain key with 0s. + copy(key, make([]byte, len(key))) + + m.transformers.Add(encKEK, transformer) + + return transformer, nil +} + +// Decrypt attempts to decrypt the ciphertext with the localKEK, a KEK from the +// store, or the remote KMS. +func (m *LocalKEKService) Decrypt(ctx context.Context, uid string, req *service.DecryptRequest) ([]byte, error) { + if _, ok := req.Annotations[referenceKEKAnnotationKey]; !ok { + return nil, fmt.Errorf("unable to find local KEK for request with uid %q", uid) + } + + transformer, err := m.getTransformerForDecryption(ctx, uid, req) + if err != nil { + klog.V(2).InfoS("decrypt ciphertext", "uid", uid, "err", err) + return nil, fmt.Errorf("failed to get transformer for decryption: %w", err) + } + + pt, _, err := transformer.TransformFromStorage(ctx, req.Ciphertext, emptyContext) + if err != nil { + klog.V(2).InfoS("decrypt ciphertext with pulled key", "uid", uid, "err", err) + return nil, err + } + + return pt, nil +} + +// Status returns the status of the remote KMS. +func (m *LocalKEKService) Status(ctx context.Context) (*service.StatusResponse, error) { + // TODO(aramase): the response from the remote KMS is funneled through without any validation/action. + // This needs to handle the case when remote KEK has changed. The local KEK needs to be rotated and + // re-encrypted with the new remote KEK. + return m.remoteKMS.Status(ctx) +} + +func annotationsWithoutReferenceKeys(annotations map[string][]byte) map[string][]byte { + if len(annotations) <= numAnnotations { + return nil + } + + m := make(map[string][]byte, len(annotations)-numAnnotations) + for k, v := range annotations { + k, v := k, v + if strings.HasSuffix(k, referenceSuffix) { + continue + } + m[k] = v + } + return m +} + +func validateRemoteKMSResponse(resp *service.EncryptResponse) error { + // validate annotations don't contain the reference implementation annotations + for k := range resp.Annotations { + if strings.HasSuffix(k, referenceSuffix) { + return errInvalidKMSAnnotationKeySuffix + } + } + return nil +} + +// generateKey generates a random key using system randomness. +func generateKey(length int) (key []byte, err error) { + key = make([]byte, length) + if _, err = rand.Read(key); err != nil { + return nil, err + } + + return key, nil +} diff --git a/staging/src/k8s.io/kms/encryption/service_test.go b/staging/src/k8s.io/kms/encryption/service_test.go new file mode 100644 index 00000000000..4a46b27fcba --- /dev/null +++ b/staging/src/k8s.io/kms/encryption/service_test.go @@ -0,0 +1,394 @@ +/* +Copyright 2023 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 encryption + +import ( + "context" + "encoding/base64" + "errors" + "reflect" + "sync" + "testing" + "time" + + "k8s.io/kms/service" +) + +func TestCopyResponseAndAddLocalKEKAnnotation(t *testing.T) { + testCases := []struct { + name string + input *service.EncryptResponse + want *service.EncryptResponse + }{ + { + name: "annotations is nil", + input: &service.EncryptResponse{ + Ciphertext: []byte("encryptedLocalKEK"), + KeyID: "keyID", + Annotations: nil, + }, + want: &service.EncryptResponse{ + KeyID: "keyID", + Annotations: map[string][]byte{ + referenceKEKAnnotationKey: []byte("encryptedLocalKEK"), + }, + }, + }, + { + name: "remote KMS sent 1 annotation", + input: &service.EncryptResponse{ + Ciphertext: []byte("encryptedLocalKEK"), + KeyID: "keyID", + Annotations: map[string][]byte{ + "version.encryption.remote.io": []byte("1"), + }, + }, + want: &service.EncryptResponse{ + KeyID: "keyID", + Annotations: map[string][]byte{ + "version.encryption.remote.io": []byte("1"), + referenceKEKAnnotationKey: []byte("encryptedLocalKEK"), + }, + }, + }, + { + name: "remote KMS sent 2 annotations", + input: &service.EncryptResponse{ + Ciphertext: []byte("encryptedLocalKEK"), + KeyID: "keyID", + Annotations: map[string][]byte{ + "version.encryption.remote.io": []byte("1"), + "key-version.encryption.remote.io": []byte("2"), + }, + }, + want: &service.EncryptResponse{ + KeyID: "keyID", + Annotations: map[string][]byte{ + "version.encryption.remote.io": []byte("1"), + "key-version.encryption.remote.io": []byte("2"), + referenceKEKAnnotationKey: []byte("encryptedLocalKEK"), + }, + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + got := copyResponseAndAddLocalKEKAnnotation(tc.input) + if !reflect.DeepEqual(got, tc.want) { + t.Errorf("copyResponseAndAddLocalKEKAnnotation(%v) = %v, want %v", tc.input, got, tc.want) + } + }) + } +} + +func TestAnnotationsWithoutReferenceKeys(t *testing.T) { + testCases := []struct { + name string + input map[string][]byte + want map[string][]byte + }{ + { + name: "annotations is nil", + input: nil, + want: nil, + }, + { + name: "annotations is empty", + input: map[string][]byte{}, + want: nil, + }, + { + name: "annotations only contains reference keys", + input: map[string][]byte{ + referenceKEKAnnotationKey: []byte("encryptedLocalKEK"), + }, + want: nil, + }, + { + name: "annotations contains 1 reference key and 1 other key", + input: map[string][]byte{ + referenceKEKAnnotationKey: []byte("encryptedLocalKEK"), + "version.encryption.remote.io": []byte("1"), + }, + want: map[string][]byte{ + "version.encryption.remote.io": []byte("1"), + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + got := annotationsWithoutReferenceKeys(tc.input) + if !reflect.DeepEqual(got, tc.want) { + t.Errorf("annotationsWithoutReferenceKeys(%v) = %v, want %v", tc.input, got, tc.want) + } + }) + } +} + +func TestValidateRemoteKMSResponse(t *testing.T) { + testCases := []struct { + name string + input *service.EncryptResponse + want error + }{ + { + name: "annotations is nil", + input: &service.EncryptResponse{}, + want: nil, + }, + { + name: "annotation key contains reference suffix", + input: &service.EncryptResponse{ + Annotations: map[string][]byte{ + "version.reference.encryption.k8s.io": []byte("1"), + }, + }, + want: errInvalidKMSAnnotationKeySuffix, + }, + { + name: "no annotation key contains reference suffix", + input: &service.EncryptResponse{ + Annotations: map[string][]byte{ + "version.encryption.remote.io": []byte("1"), + "key-version.encryption.remote.io": []byte("2"), + }, + }, + want: nil, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + got := validateRemoteKMSResponse(tc.input) + if got != tc.want { + t.Errorf("validateRemoteKMSResponse(%v) = %v, want %v", tc.input, got, tc.want) + } + }) + } +} + +var _ service.Service = &testRemoteService{} + +type testRemoteService struct { + mu sync.Mutex + + keyID string + disabled bool +} + +func (s *testRemoteService) Encrypt(ctx context.Context, uid string, plaintext []byte) (*service.EncryptResponse, error) { + s.mu.Lock() + defer s.mu.Unlock() + + if s.disabled { + return nil, errors.New("failed to encrypt") + } + return &service.EncryptResponse{ + KeyID: s.keyID, + Ciphertext: []byte(base64.StdEncoding.EncodeToString(plaintext)), + Annotations: map[string][]byte{ + "version.encryption.remote.io": []byte("1"), + }, + }, nil +} + +func (s *testRemoteService) Decrypt(ctx context.Context, uid string, req *service.DecryptRequest) ([]byte, error) { + s.mu.Lock() + defer s.mu.Unlock() + + if s.disabled { + return nil, errors.New("failed to decrypt") + } + if len(req.Annotations) != 1 { + return nil, errors.New("invalid annotations") + } + if v, ok := req.Annotations["version.encryption.remote.io"]; !ok || string(v) != "1" { + return nil, errors.New("invalid version in annotations") + } + return base64.StdEncoding.DecodeString(string(req.Ciphertext)) +} + +func (s *testRemoteService) Status(ctx context.Context) (*service.StatusResponse, error) { + s.mu.Lock() + defer s.mu.Unlock() + + if s.disabled { + return nil, errors.New("failed to get status") + } + return &service.StatusResponse{ + Version: "v2alpha1", + Healthz: "ok", + KeyID: s.keyID, + }, nil +} + +func (s *testRemoteService) SetDisabledStatus(disabled bool) { + s.mu.Lock() + defer s.mu.Unlock() + s.disabled = true +} + +func TestEncrypt(t *testing.T) { + remoteKMS := &testRemoteService{keyID: "test-key-id"} + localKEKService := NewLocalKEKService(remoteKMS) + + validateResponse := func(got *service.EncryptResponse, t *testing.T) { + if len(got.Annotations) != 2 { + t.Fatalf("Encrypt() annotations = %v, want 2 annotations", got.Annotations) + } + if _, ok := got.Annotations[referenceKEKAnnotationKey]; !ok { + t.Fatalf("Encrypt() annotations = %v, want %v", got.Annotations, referenceKEKAnnotationKey) + } + if got.KeyID != remoteKMS.keyID { + t.Fatalf("Encrypt() keyID = %v, want %v", got.KeyID, remoteKMS.keyID) + } + if localKEKService.localTransformer == nil { + t.Fatalf("Encrypt() localTransformer = %v, want non-nil", localKEKService.localTransformer) + } + } + + ctx := testContext(t) + // local KEK is generated and encryption is successful + got, err := localKEKService.Encrypt(ctx, "test-uid", []byte("test-plaintext")) + if err != nil { + t.Fatalf("Encrypt() error = %v", err) + } + validateResponse(got, t) + + // local KEK is used for encryption even when remote KMS is failing + remoteKMS.SetDisabledStatus(true) + if got, err = localKEKService.Encrypt(ctx, "test-uid", []byte("test-plaintext")); err != nil { + t.Fatalf("Encrypt() error = %v", err) + } + validateResponse(got, t) +} + +func TestEncryptError(t *testing.T) { + remoteKMS := &testRemoteService{keyID: "test-key-id"} + localKEKService := NewLocalKEKService(remoteKMS) + + ctx := testContext(t) + + localKEKGenerationPollTimeout = 5 * time.Second + // first time local KEK generation fails because of remote KMS + remoteKMS.SetDisabledStatus(true) + _, err := localKEKService.Encrypt(ctx, "test-uid", []byte("test-plaintext")) + if err == nil { + t.Fatalf("Encrypt() error = %v, want non-nil", err) + } + if localKEKService.localTransformer != nil { + t.Fatalf("Encrypt() localTransformer = %v, want nil", localKEKService.localTransformer) + } + + remoteKMS.SetDisabledStatus(false) +} + +func TestDecrypt(t *testing.T) { + remoteKMS := &testRemoteService{keyID: "test-key-id"} + localKEKService := NewLocalKEKService(remoteKMS) + + ctx := testContext(t) + + // local KEK is generated and encryption/decryption is successful + got, err := localKEKService.Encrypt(ctx, "test-uid", []byte("test-plaintext")) + if err != nil { + t.Fatalf("Encrypt() error = %v", err) + } + if string(got.Ciphertext) == "test-plaintext" { + t.Fatalf("Encrypt() ciphertext = %v, want it to be encrypted", got.Ciphertext) + } + decryptRequest := &service.DecryptRequest{ + Ciphertext: got.Ciphertext, + Annotations: got.Annotations, + KeyID: got.KeyID, + } + plaintext, err := localKEKService.Decrypt(ctx, "test-uid", decryptRequest) + if err != nil { + t.Fatalf("Decrypt() error = %v", err) + } + if string(plaintext) != "test-plaintext" { + t.Fatalf("Decrypt() plaintext = %v, want %v", string(plaintext), "test-plaintext") + } + + // local KEK is used for decryption even when remote KMS is failing + remoteKMS.SetDisabledStatus(true) + if _, err = localKEKService.Decrypt(ctx, "test-uid", decryptRequest); err != nil { + t.Fatalf("Decrypt() error = %v", err) + } +} + +func TestDecryptError(t *testing.T) { + remoteKMS := &testRemoteService{keyID: "test-key-id"} + localKEKService := NewLocalKEKService(remoteKMS) + + ctx := testContext(t) + + got, err := localKEKService.Encrypt(ctx, "test-uid", []byte("test-plaintext")) + if err != nil { + t.Fatalf("Encrypt() error = %v", err) + } + decryptRequest := &service.DecryptRequest{ + Ciphertext: got.Ciphertext, + Annotations: got.Annotations, + KeyID: got.KeyID, + } + // local KEK for decryption not in cache and remote KMS is failing + remoteKMS.SetDisabledStatus(true) + // clear the cache + localKEKService.transformers.Clear() + if _, err = localKEKService.Decrypt(ctx, "test-uid", decryptRequest); err == nil { + t.Fatalf("Decrypt() error = %v, want non-nil", err) + } +} + +func TestStatus(t *testing.T) { + remoteKMS := &testRemoteService{keyID: "test-key-id"} + localKEKService := NewLocalKEKService(remoteKMS) + + ctx := testContext(t) + + got, err := localKEKService.Status(ctx) + if err != nil { + t.Fatalf("Status() error = %v", err) + } + if got.Version != "v2alpha1" { + t.Fatalf("Status() version = %v, want %v", got.Version, "v2alpha1") + } + if got.Healthz != "ok" { + t.Fatalf("Status() healthz = %v, want %v", got.Healthz, "ok") + } + if got.KeyID != "test-key-id" { + t.Fatalf("Status() keyID = %v, want %v", got.KeyID, "test-key-id") + } + + // remote KMS is failing + remoteKMS.SetDisabledStatus(true) + if _, err = localKEKService.Status(ctx); err == nil { + t.Fatalf("Status() error = %v, want non-nil", err) + } +} + +func testContext(t *testing.T) context.Context { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + return ctx +} diff --git a/staging/src/k8s.io/kms/go.mod b/staging/src/k8s.io/kms/go.mod index ec13d7bb6b5..e4eb9af5401 100644 --- a/staging/src/k8s.io/kms/go.mod +++ b/staging/src/k8s.io/kms/go.mod @@ -9,6 +9,7 @@ require ( google.golang.org/grpc v1.51.0 k8s.io/apimachinery v0.0.0 k8s.io/klog/v2 v2.80.1 + k8s.io/utils v0.0.0-20230209194617-a36077c30491 ) require ( @@ -19,7 +20,6 @@ require ( golang.org/x/text v0.6.0 // indirect google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21 // indirect google.golang.org/protobuf v1.28.1 // indirect - k8s.io/utils v0.0.0-20230209194617-a36077c30491 // indirect ) replace ( diff --git a/staging/src/k8s.io/kms/pkg/encrypt/aes/aes.go b/staging/src/k8s.io/kms/pkg/encrypt/aes/aes.go new file mode 100644 index 00000000000..fad47948737 --- /dev/null +++ b/staging/src/k8s.io/kms/pkg/encrypt/aes/aes.go @@ -0,0 +1,85 @@ +/* +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. +*/ + +// Vendored from kubernetes/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/aes/aes.go +// * commit: 90b42f91fd904b71fd52ca9ae55a5de73e6b779a +// * link: https://github.com/kubernetes/kubernetes/blob/90b42f91fd904b71fd52ca9ae55a5de73e6b779a/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/aes/aes.go + +// Package aes transforms values for storage at rest using AES-GCM. +package aes + +import ( + "context" + "crypto/cipher" + "crypto/rand" + "fmt" + + "k8s.io/kms/pkg/value" +) + +// gcm implements AEAD encryption of the provided values given a cipher.Block algorithm. +// The authenticated data provided as part of the value.Context method must match when the same +// value is set to and loaded from storage. In order to ensure that values cannot be copied by +// an attacker from a location under their control, use characteristics of the storage location +// (such as the etcd key) as part of the authenticated data. +// +// Because this mode requires a generated IV and IV reuse is a known weakness of AES-GCM, keys +// must be rotated before a birthday attack becomes feasible. NIST SP 800-38D +// (http://csrc.nist.gov/publications/nistpubs/800-38D/SP-800-38D.pdf) recommends using the same +// key with random 96-bit nonces (the default nonce length) no more than 2^32 times, and +// therefore transformers using this implementation *must* ensure they allow for frequent key +// rotation. Future work should include investigation of AES-GCM-SIV as an alternative to +// random nonces. +type gcm struct { + block cipher.Block +} + +// NewGCMTransformer takes the given block cipher and performs encryption and decryption on the given +// data. +func NewGCMTransformer(block cipher.Block) value.Transformer { + return &gcm{block: block} +} + +func (t *gcm) TransformFromStorage(ctx context.Context, data []byte, dataCtx value.Context) ([]byte, bool, error) { + aead, err := cipher.NewGCM(t.block) + if err != nil { + return nil, false, err + } + nonceSize := aead.NonceSize() + if len(data) < nonceSize { + return nil, false, fmt.Errorf("the stored data was shorter than the required size") + } + result, err := aead.Open(nil, data[:nonceSize], data[nonceSize:], dataCtx.AuthenticatedData()) + return result, false, err +} + +func (t *gcm) TransformToStorage(ctx context.Context, data []byte, dataCtx value.Context) ([]byte, error) { + aead, err := cipher.NewGCM(t.block) + if err != nil { + return nil, err + } + nonceSize := aead.NonceSize() + result := make([]byte, nonceSize+aead.Overhead()+len(data)) + n, err := rand.Read(result[:nonceSize]) + if err != nil { + return nil, err + } + if n != nonceSize { + return nil, fmt.Errorf("unable to read sufficient random bytes") + } + cipherText := aead.Seal(result[nonceSize:nonceSize], result[:nonceSize], data, dataCtx.AuthenticatedData()) + return result[:nonceSize+len(cipherText)], nil +} diff --git a/staging/src/k8s.io/kms/pkg/value/interface.go b/staging/src/k8s.io/kms/pkg/value/interface.go new file mode 100644 index 00000000000..d7ad3013fe5 --- /dev/null +++ b/staging/src/k8s.io/kms/pkg/value/interface.go @@ -0,0 +1,49 @@ +/* +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 value + +import "context" + +// Vendored from kubernetes/staging/src/k8s.io/apiserver/pkg/storage/value/transformer.go +// * commit: 59e1a32fc8ed35e328a3971d3a1d640ffc28ff55 +// * link: https://github.com/kubernetes/kubernetes/blob/59e1a32fc8ed35e328a3971d3a1d640ffc28ff55/staging/src/k8s.io/apiserver/pkg/storage/value/transformer.go + +// Transformer allows a value to be transformed before being read from or written to the underlying store. The methods +// must be able to undo the transformation caused by the other. +type Transformer interface { + // TransformFromStorage may transform the provided data from its underlying storage representation or return an error. + // Stale is true if the object on disk is stale and a write to etcd should be issued, even if the contents of the object + // have not changed. + TransformFromStorage(ctx context.Context, data []byte, dataCtx Context) (out []byte, stale bool, err error) + // TransformToStorage may transform the provided data into the appropriate form in storage or return an error. + TransformToStorage(ctx context.Context, data []byte, dataCtx Context) (out []byte, err error) +} + +// Context is additional information that a storage transformation may need to verify the data at rest. +type Context interface { + // AuthenticatedData should return an array of bytes that describes the current value. If the value changes, + // the transformer may report the value as unreadable or tampered. This may be nil if no such description exists + // or is needed. For additional verification, set this to data that strongly identifies the value, such as + // the key and creation version of the stored data. + AuthenticatedData() []byte +} + +// DefaultContext is a simple implementation of Context for a slice of bytes. +type DefaultContext []byte + +// AuthenticatedData returns itself. +func (c DefaultContext) AuthenticatedData() []byte { return c }