diff --git a/staging/src/k8s.io/kms/encryption/service.go b/staging/src/k8s.io/kms/encryption/service.go index caef5e773b5..89bc3d7dc0a 100644 --- a/staging/src/k8s.io/kms/encryption/service.go +++ b/staging/src/k8s.io/kms/encryption/service.go @@ -18,180 +18,245 @@ package encryption import ( "context" - "errors" + "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 ( - // ErrNoCipher means that there is no remote kms given and therefore the keys in use can't be protected. - ErrNoCipher = errors.New("no remote encryption service was specified") - // EmptyContext is an empty slice of bytes. - EmptyContext = DefaultContext([]byte{}) - // LocalKEKID is the key used to store the localKEK in the annotations. - LocalKEKID = "kmsv2:local-kek" + // 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 KEKs are stored as transformers in the local store. The encrypted -// form of the KEK is used to pick a transformer from the store. The KEKs should -// be encrypted by the remote KMS. -// There is a distinguished KEK (localKEK), that is generated and used by the -// LocalKEKService to encrypt. +// 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 the local KEKs. - remoteKMS service.Service - // remoteKMSID is the ID that helps remoteKMS to decrypt localKEKID. - remoteKMSID string - // localKEKID is the localKEK in encrypted form. - localKEKID []byte + // remoteKMS is the remote kms that is used to encrypt and decrypt the local KEKs. + remoteKMS service.Service + remoteOnce sync.Once - // transformers is a store that holds all known transformers. - transformers Store - // createTransformer creates a new transformer and appropriate keys. - createTransformer CreateTransformer - // createUID creates a new uid. - createUID func() (string, error) + // 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 key that is encrypted by the -// remoteService. In the current implementation, the localKEK Service needs to be +// 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. -func NewLocalKEKService( - ctx context.Context, - remoteService service.Service, - store Store, - createTransformer CreateTransformer, - createUID func() (string, error), // TODO add sensible defaults, use functional options -) (*LocalKEKService, error) { - if remoteService == nil { - klog.V(2).InfoS("can't create LocalKEKService without remoteService") - return nil, ErrNoCipher - } - - key, err := createTransformer.Key() - if err != nil { - klog.V(2).InfoS("create key", "err", err) - return nil, err - } - - transformer, err := createTransformer.Transformer(ctx, key) - if err != nil { - klog.V(2).InfoS("create new cipher", "err", err) - return nil, err - } - - uid, err := createUID() - if err != nil { - klog.V(2).InfoS("create new uid", "err", err) - return nil, err - } - - encRes, err := remoteService.Encrypt(ctx, uid, key) - if err != nil { - klog.V(2).InfoS("encrypt with remote", "err", err) - return nil, err - } - - store.Add(encRes.Ciphertext, transformer) - +// 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{ - remoteKMSID: encRes.KeyID, - remoteKMS: remoteService, - localKEKID: encRes.Ciphertext, - - transformers: store, - createTransformer: createTransformer, - createUID: createUID, - }, nil + remoteKMS: remoteService, + transformers: lru.New(cacheSize), + } } -// getTransformer returns the transformer for the given keyID. If the keyID is -// not known, the key gets decrypted by the remoteKMS. -func (m *LocalKEKService) getTransformer(ctx context.Context, encKey []byte, uid, keyID string) (Transformer, error) { - transformer, ok := m.transformers.Get(encKey) - if ok { - return transformer, nil - } +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) - // Decrypt the unknown key with remote KMS. Plainkey must be treated with secrecy. - plainKey, err := m.remoteKMS.Decrypt(ctx, uid, &service.DecryptRequest{ - Ciphertext: encKey, - KeyID: keyID, + 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 + }) }) - if err != nil { - klog.V(2).InfoS("decrypt key with remote key", "id", uid, "err", err) + return m.localTransformer, m.remoteKMSResponse, m.localTransformerErr +} - return nil, err +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 - t, err := m.createTransformer.Transformer(ctx, plainKey) - if err != nil { - klog.V(2).InfoS("create transformer", "id", uid, "err", err) - return nil, err + return &service.EncryptResponse{ + // Ciphertext is not set on purpose - it is different per Encrypt call + KeyID: resp.KeyID, + Annotations: annotations, } - - // Overwrite the plain key with 0s. - copy(plainKey, make([]byte, len(plainKey))) - - m.transformers.Add(encKey, t) - - return t, nil } // Encrypt encrypts the plaintext with the localKEK. func (m *LocalKEKService) Encrypt(ctx context.Context, uid string, pt []byte) (*service.EncryptResponse, error) { - // It could happen that the localKEK is not available, if the store is an expiring cache. - transformer, err := m.getTransformer(ctx, m.localKEKID, uid, m.remoteKMSID) + transformer, resp, err := m.getTransformerForEncryption(uid) if err != nil { - klog.V(2).InfoS("encrypt plaintext", "id", uid, "err", err) + klog.V(2).InfoS("encrypt plaintext", "uid", uid, "err", err) return nil, err } - ct, err := transformer.TransformToStorage(ctx, pt, EmptyContext) + ct, err := transformer.TransformToStorage(ctx, pt, emptyContext) if err != nil { - klog.V(2).InfoS("encrypt plaintext", "id", uid, "err", err) + klog.V(2).InfoS("encrypt plaintext", "uid", uid, "err", err) return nil, err } return &service.EncryptResponse{ - Ciphertext: ct, - KeyID: m.remoteKMSID, - Annotations: map[string][]byte{ - LocalKEKID: m.localKEKID, - }, + 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) { - encKEK, ok := req.Annotations[LocalKEKID] - if !ok { - // If there is no local KEK ID in the annotations, we must delegate to remote KMS. - pt, err := m.remoteKMS.Decrypt(ctx, uid, req) - if err != nil { - klog.V(2).InfoS("decrypt key with remote key", "id", uid, "err", err) - - return nil, err - } - - return pt, nil + if _, ok := req.Annotations[referenceKEKAnnotationKey]; !ok { + return nil, fmt.Errorf("unable to find local KEK for request with uid %q", uid) } - transformer, err := m.getTransformer(ctx, encKEK, uid, req.KeyID) + transformer, err := m.getTransformerForDecryption(ctx, uid, req) if err != nil { - klog.V(2).InfoS("decrypt ciphertext", "id", uid, "err", err) - return nil, err + 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) + pt, _, err := transformer.TransformFromStorage(ctx, req.Ciphertext, emptyContext) if err != nil { - klog.V(2).InfoS("decrypt ciphertext with pulled key", "id", uid, "err", err) + 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 index d1f6178df82..4a46b27fcba 100644 --- a/staging/src/k8s.io/kms/encryption/service_test.go +++ b/staging/src/k8s.io/kms/encryption/service_test.go @@ -17,671 +17,378 @@ limitations under the License. package encryption import ( - "bytes" "context" + "encoding/base64" "errors" + "reflect" + "sync" "testing" + "time" "k8s.io/kms/service" ) -var ( - errCreateKey = errors.New("can't create key") - errCreateTransformer = errors.New("can't create transformer") - errCreateUID = errors.New("can't create uid") - errRemoteEncrypt = errors.New("can't encrypt with remote kms") - errRemoteDecrypt = errors.New("can't decrypt with remote kms") - errLocallyEncrypt = errors.New("can't encrypt with local kms") - errLocallyDecrypt = errors.New("can't decrypt with local kms") - errStoreDecrypt = errors.New("can't decrypt with local store") - errWrongValue = errors.New("wrong value") - errNotBeCalled = errors.New("should not be called") - - keyID = "id:123" - genKey = []byte("genkey:123") - storeKey = []byte("storekey:123") - anonKey = []byte("anonkey:123") - plainKey = []byte("plainkey:123") - encryptKey = []byte("enckey:123") - decryptKey = []byte("deckey:123") - encryptMessage = []byte("encrypt_to_storage") - decryptMessage = []byte("decrypt_from_storage") -) - -func TestService(t *testing.T) { - t.Parallel() - - initTestCases := []struct { - name string - localKEKService func() (*LocalKEKService, error) - err error +func TestCopyResponseAndAddLocalKEKAnnotation(t *testing.T) { + testCases := []struct { + name string + input *service.EncryptResponse + want *service.EncryptResponse }{ { - name: "should fail if no remote service is given", - localKEKService: func() (*LocalKEKService, error) { - return NewLocalKEKService( - context.Background(), - nil, nil, nil, nil, - ) + name: "annotations is nil", + input: &service.EncryptResponse{ + Ciphertext: []byte("encryptedLocalKEK"), + KeyID: "keyID", + Annotations: nil, }, - err: ErrNoCipher, - }, - { - name: "should fail if you can't create a key", - localKEKService: func() (*LocalKEKService, error) { - return NewLocalKEKService( - context.Background(), - &testService{}, - nil, - &createTransformer{ - key: func() ([]byte, error) { - return nil, errCreateKey - }, - }, - nil, - ) - }, - err: errCreateKey, - }, - { - name: "should fail if you can't create a transformer", - localKEKService: func() (*LocalKEKService, error) { - return NewLocalKEKService( - context.Background(), - &testService{}, - nil, - &createTransformer{ - key: genLocalKey, - transformer: func(ctx context.Context, key []byte) (Transformer, error) { - if !bytes.Equal(key, genKey) { - return nil, errWrongValue - } - - return nil, errCreateTransformer - }, - }, - nil, - ) - }, - err: errCreateTransformer, - }, - { - name: "should fail if you can't create a uid", - localKEKService: func() (*LocalKEKService, error) { - return NewLocalKEKService( - context.Background(), - &testService{}, - nil, - &createTransformer{key: genLocalKey, transformer: makeEmptyTransformer}, - func() (string, error) { - return "", errCreateUID - }, - ) - }, - err: errCreateUID, - }, - { - name: "should fail if you can't encrypt remotely", - localKEKService: func() (*LocalKEKService, error) { - return NewLocalKEKService( - context.Background(), - &testService{ - encrypt: func(ctx context.Context, uid string, key []byte) (*service.EncryptResponse, error) { - return nil, errRemoteEncrypt - }, - }, - nil, - &createTransformer{key: genLocalKey, transformer: makeEmptyTransformer}, - genUID, - ) - }, - err: errRemoteEncrypt, - }, - { - name: "should succeed initializing", - localKEKService: func() (*LocalKEKService, error) { - return NewLocalKEKService( - context.Background(), - &testService{encrypt: remoteEncryptDefault}, - makeEmptyStore(), - &createTransformer{key: genLocalKey, transformer: makeEmptyTransformer}, - genUID, - ) - }, - }, - } - - for _, tc := range initTestCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - localKEKService, err := tc.localKEKService() - if err == tc.err { - return - } - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if localKEKService == nil { - t.Fatalf("unexpected error: %v", err) - } - }) - } - - encryptTestCases := []struct { - name string - localKEKService func() (*LocalKEKService, error) - err error - }{ - { - name: "should fail if local kek is not in cache and remote decrypt fails", - localKEKService: func() (*LocalKEKService, error) { - return NewLocalKEKService( - context.Background(), - &testService{ - encrypt: remoteEncryptDefault, - decrypt: func(ctx context.Context, uid string, decReq *service.DecryptRequest) ([]byte, error) { - return nil, errRemoteDecrypt - }, - }, - makeEmptyStore(), - &createTransformer{ - key: genLocalKey, - transformer: func(context.Context, []byte) (Transformer, error) { - return &testTransformer{ - transformToStorage: func(ctx context.Context, data []byte, dataCtx Context) ([]byte, error) { - return encryptMessage, nil - }, - }, nil - }, - }, - genUID, - ) - }, - err: errRemoteDecrypt, - }, - { - name: "should fail if you can't encrypt locally", - localKEKService: func() (*LocalKEKService, error) { - return NewLocalKEKService( - context.Background(), - &testService{encrypt: remoteEncryptDefault, decrypt: remoteDecryptShouldNotBeCalled}, - &testStore{ - add: func([]byte, Transformer) {}, - get: func(encKey []byte) (Transformer, bool) { - return &testTransformer{ - transformToStorage: func(ctx context.Context, data []byte, dataCtx Context) ([]byte, error) { - return nil, errLocallyEncrypt - }, - }, true - }, - }, - &createTransformer{ - key: genLocalKey, - transformer: func(context.Context, []byte) (Transformer, error) { - return &testTransformer{}, nil - }, - }, - genUID, - ) - }, - err: errLocallyEncrypt, - }, - { - name: "should succeed encrypting with stored transformer", - localKEKService: func() (*LocalKEKService, error) { - return NewLocalKEKService( - context.Background(), - &testService{encrypt: remoteEncryptDefault, decrypt: remoteDecryptShouldNotBeCalled}, - &testStore{ - add: func([]byte, Transformer) {}, - get: func(encKey []byte) (Transformer, bool) { - return &testTransformer{ - transformToStorage: func(ctx context.Context, data []byte, dataCtx Context) ([]byte, error) { - return encryptMessage, nil - }, - }, true - }, - }, - &createTransformer{ - key: genLocalKey, - transformer: func(context.Context, []byte) (Transformer, error) { - return &testTransformer{}, nil - }, - }, - genUID, - ) - }, - }, - { - name: "should succeed encrypting with stored transformer", - localKEKService: func() (*LocalKEKService, error) { - return NewLocalKEKService( - context.Background(), - &testService{encrypt: remoteEncryptDefault, decrypt: remoteDecryptShouldNotBeCalled}, - &testStore{ - add: func([]byte, Transformer) {}, - get: func(encKey []byte) (Transformer, bool) { - return &testTransformer{ - transformToStorage: func(ctx context.Context, data []byte, dataCtx Context) ([]byte, error) { - return encryptMessage, nil - }, - }, true - }, - }, - &createTransformer{ - key: genLocalKey, - transformer: func(context.Context, []byte) (Transformer, error) { - return &testTransformer{}, nil - }, - }, - genUID, - ) - }, - }, - { - name: "should succeed encrypting with created transformer", - localKEKService: func() (*LocalKEKService, error) { - return NewLocalKEKService( - context.Background(), - &testService{ - encrypt: remoteEncryptDefault, - decrypt: func(ctx context.Context, uid string, decReq *service.DecryptRequest) ([]byte, error) { - return encryptKey, nil - }, - }, - &testStore{ - add: func([]byte, Transformer) {}, - get: func(encKey []byte) (Transformer, bool) { - return nil, false - }, - }, - &createTransformer{ - key: genLocalKey, - transformer: func(context.Context, []byte) (Transformer, error) { - return &testTransformer{ - transformToStorage: func(ctx context.Context, data []byte, dataCtx Context) ([]byte, error) { - return encryptMessage, nil - }, - }, nil - }, - }, - genUID, - ) - }, - }, - } - - for _, tc := range encryptTestCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - localKEKService, err := tc.localKEKService() - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - encRes, err := localKEKService.Encrypt(context.Background(), "id:999", []byte("message")) - if err == tc.err { - return - } - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if !bytes.Equal(encryptMessage, encRes.Ciphertext) { - t.Fatalf("unexpected ciphertext - want: %s, have: %s", encryptMessage, encRes.Ciphertext) - } - }) - } - - noAnnotationsDecReq := &service.DecryptRequest{ - Ciphertext: encryptMessage, - KeyID: keyID, - Annotations: map[string][]byte{}, - } - - anonDecReq := &service.DecryptRequest{ - Ciphertext: encryptMessage, - KeyID: keyID, - Annotations: map[string][]byte{ - LocalKEKID: anonKey, - }, - } - - decryptTestCases := []struct { - name string - localKEKService func() (*LocalKEKService, error) - decReq *service.DecryptRequest - err error - }{ - { - name: "should fail decrypting without annotations, if remote decryption doesn't work", - localKEKService: func() (*LocalKEKService, error) { - return NewLocalKEKService( - context.Background(), - &testService{ - encrypt: remoteEncryptDefault, - decrypt: func(ctx context.Context, uid string, req *service.DecryptRequest) ([]byte, error) { - return nil, errRemoteDecrypt - }, - }, - makeEmptyStore(), - &createTransformer{key: genLocalKey, transformer: makeEmptyTransformer}, - genUID, - ) - }, - decReq: noAnnotationsDecReq, - err: errRemoteDecrypt, - }, - { - name: "should succeed decrypting without annotations, if remote decryption works", - localKEKService: func() (*LocalKEKService, error) { - return NewLocalKEKService( - context.Background(), - &testService{ - encrypt: remoteEncryptDefault, - decrypt: func(ctx context.Context, uid string, req *service.DecryptRequest) ([]byte, error) { - return decryptMessage, nil - }, - }, - makeEmptyStore(), - &createTransformer{key: genLocalKey, transformer: makeEmptyTransformer}, - genUID, - ) - }, - decReq: noAnnotationsDecReq, - }, - { - name: "should fail decrypting, if decrypting with remote KMS doesn't work for unknown key", - localKEKService: func() (*LocalKEKService, error) { - return NewLocalKEKService( - context.Background(), - &testService{ - encrypt: remoteEncryptDefault, - decrypt: func(ctx context.Context, uid string, req *service.DecryptRequest) ([]byte, error) { - return nil, errRemoteDecrypt - }, - }, - &testStore{ - add: func([]byte, Transformer) {}, - get: func(encKey []byte) (Transformer, bool) { return nil, false }, - }, - &createTransformer{ - key: genLocalKey, - transformer: func(context.Context, []byte) (Transformer, error) { - return &testTransformer{ - transformFromStorage: func(ctx context.Context, ct []byte, defaultCtx Context) ([]byte, bool, error) { - return nil, false, errNotBeCalled - }, - }, nil - }, - }, - genUID, - ) - }, - decReq: anonDecReq, - err: errRemoteDecrypt, - }, - { - name: "should fail decrypting, if creating a transformer fails with unknown key", - localKEKService: func() (*LocalKEKService, error) { - return NewLocalKEKService( - context.Background(), - &testService{ - encrypt: remoteEncryptDefault, - decrypt: func(ctx context.Context, uid string, req *service.DecryptRequest) ([]byte, error) { - return plainKey, nil - }, - }, - &testStore{ - add: func(key []byte, transformer Transformer) {}, - get: func(key []byte) (Transformer, bool) { - return nil, false - }, - }, - &createTransformer{ - key: genLocalKey, - transformer: func(ctx context.Context, key []byte) (Transformer, error) { - if bytes.Equal(key, plainKey) { - return nil, errCreateTransformer - } - - return &testTransformer{}, nil - }, - }, - genUID, - ) - }, - decReq: &service.DecryptRequest{ - Ciphertext: encryptMessage, - KeyID: keyID, + want: &service.EncryptResponse{ + KeyID: "keyID", Annotations: map[string][]byte{ - LocalKEKID: anonKey, - }, - }, - err: errCreateTransformer, - }, - { - name: "should fail decrypting, if decryption fails with stored transformer", - localKEKService: func() (*LocalKEKService, error) { - return NewLocalKEKService( - context.Background(), - &testService{ - encrypt: remoteEncryptDefault, - decrypt: remoteDecryptShouldNotBeCalled, - }, - &testStore{ - add: func(key []byte, transformer Transformer) {}, - get: func(key []byte) (Transformer, bool) { - if !bytes.Equal(key, storeKey) { - return nil, false - } - - return &testTransformer{ - transformFromStorage: func(ctx context.Context, ct []byte, defaultCtx Context) ([]byte, bool, error) { - return nil, false, errLocallyDecrypt - }, - }, true - }, - }, - &createTransformer{key: genLocalKey, transformer: makeEmptyTransformer}, - genUID, - ) - }, - decReq: &service.DecryptRequest{ - Ciphertext: encryptMessage, - KeyID: keyID, - Annotations: map[string][]byte{ - LocalKEKID: storeKey, - }, - }, - err: errLocallyDecrypt, - }, - { - name: "should succeed decrypting, if decryption works with stored transformer", - localKEKService: func() (*LocalKEKService, error) { - return NewLocalKEKService( - context.Background(), - &testService{ - encrypt: remoteEncryptDefault, - decrypt: remoteDecryptShouldNotBeCalled, - }, - &testStore{ - add: func(key []byte, transformer Transformer) {}, - get: func(key []byte) (Transformer, bool) { - if !bytes.Equal(key, storeKey) { - return nil, false - } - - return &testTransformer{ - transformFromStorage: func(ctx context.Context, ct []byte, defaultCtx Context) ([]byte, bool, error) { - return decryptMessage, false, nil - }, - }, true - }, - }, - &createTransformer{key: genLocalKey, transformer: makeEmptyTransformer}, - genUID, - ) - }, - decReq: &service.DecryptRequest{ - Ciphertext: encryptMessage, - KeyID: keyID, - Annotations: map[string][]byte{ - LocalKEKID: storeKey, + referenceKEKAnnotationKey: []byte("encryptedLocalKEK"), }, }, }, { - name: "should succeed decrypting, with newly created transformer", - localKEKService: func() (*LocalKEKService, error) { - return NewLocalKEKService( - context.Background(), - &testService{ - encrypt: remoteEncryptDefault, - decrypt: func(ctx context.Context, uid string, req *service.DecryptRequest) ([]byte, error) { - return plainKey, nil - }, - }, - makeEmptyStore(), - &createTransformer{ - key: genLocalKey, - transformer: func(ctx context.Context, key []byte) (Transformer, error) { - return &testTransformer{ - transformFromStorage: func(ctx context.Context, ct []byte, defaultCtx Context) ([]byte, bool, error) { - return decryptMessage, false, nil - }, - }, nil - }, - }, - genUID, - ) + 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"), + }, }, - decReq: anonDecReq, }, } - for _, tc := range decryptTestCases { + for _, tc := range testCases { tc := tc t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - localKEKService, err := tc.localKEKService() - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - pt, err := localKEKService.Decrypt(context.Background(), tc.name, tc.decReq) - if err == tc.err { - return - } - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if !bytes.Equal(pt, decryptMessage) { - t.Fatalf("unexpected plaintext - want: %s, have: %s", decryptMessage, pt) + got := copyResponseAndAddLocalKEKAnnotation(tc.input) + if !reflect.DeepEqual(got, tc.want) { + t.Errorf("copyResponseAndAddLocalKEKAnnotation(%v) = %v, want %v", tc.input, got, tc.want) } }) } } -type testStore struct { - add func([]byte, Transformer) - get func([]byte) (Transformer, bool) -} - -var _ Store = (*testStore)(nil) - -func (t *testStore) Add(key []byte, transformer Transformer) { - t.add(key, transformer) -} - -func (t *testStore) Get(key []byte) (Transformer, bool) { - return t.get(key) -} - -type testTransformer struct { - transformFromStorage func(context.Context, []byte, Context) ([]byte, bool, error) - transformToStorage func(context.Context, []byte, Context) ([]byte, error) -} - -var _ Transformer = (*testTransformer)(nil) - -func (t *testTransformer) TransformFromStorage(ctx context.Context, data []byte, context Context) ([]byte, bool, error) { - return t.transformFromStorage(ctx, data, context) -} - -func (t *testTransformer) TransformToStorage(ctx context.Context, data []byte, context Context) ([]byte, error) { - return t.transformToStorage(ctx, data, context) -} - -type createTransformer struct { - transformer func(context.Context, []byte) (Transformer, error) - key func() ([]byte, error) -} - -var _ CreateTransformer = (*createTransformer)(nil) - -func (c *createTransformer) Transformer(ctx context.Context, key []byte) (Transformer, error) { - return c.transformer(ctx, key) -} - -func (c *createTransformer) Key() ([]byte, error) { - return c.key() -} - -type testService struct { - decrypt func(ctx context.Context, uid string, req *service.DecryptRequest) ([]byte, error) - encrypt func(ctx context.Context, uid string, data []byte) (*service.EncryptResponse, error) - status func(ctx context.Context) (*service.StatusResponse, error) -} - -var _ service.Service = (*testService)(nil) - -func (s *testService) Decrypt(ctx context.Context, uid string, req *service.DecryptRequest) ([]byte, error) { - return s.decrypt(ctx, uid, req) -} - -func (s *testService) Encrypt(ctx context.Context, uid string, data []byte) (*service.EncryptResponse, error) { - return s.encrypt(ctx, uid, data) -} - -func (s *testService) Status(ctx context.Context) (*service.StatusResponse, error) { - return s.status(ctx) -} - -func genUID() (string, error) { - return "id:001", nil -} - -func genLocalKey() ([]byte, error) { - return genKey, nil -} - -func makeEmptyTransformer(context.Context, []byte) (Transformer, error) { - return &testTransformer{}, nil -} - -func makeEmptyStore() *testStore { - return &testStore{ - add: func([]byte, Transformer) {}, - get: func(key []byte) (Transformer, bool) { - return nil, false +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 remoteEncryptDefault(ctx context.Context, uid string, key []byte) (*service.EncryptResponse, error) { +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{ - Ciphertext: encryptKey, - KeyID: keyID, - Annotations: map[string][]byte{}, + KeyID: s.keyID, + Ciphertext: []byte(base64.StdEncoding.EncodeToString(plaintext)), + Annotations: map[string][]byte{ + "version.encryption.remote.io": []byte("1"), + }, }, nil } -func remoteDecryptShouldNotBeCalled(ctx context.Context, uid string, req *service.DecryptRequest) ([]byte, error) { - return nil, errNotBeCalled +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/encryption/interface.go b/staging/src/k8s.io/kms/pkg/value/interface.go similarity index 71% rename from staging/src/k8s.io/kms/encryption/interface.go rename to staging/src/k8s.io/kms/pkg/value/interface.go index 8ad755646a4..d7ad3013fe5 100644 --- a/staging/src/k8s.io/kms/encryption/interface.go +++ b/staging/src/k8s.io/kms/pkg/value/interface.go @@ -1,5 +1,5 @@ /* -Copyright 2023 The Kubernetes Authors. +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. @@ -14,32 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -package encryption +package value import "context" -// Store is a simple interface to store and retrieve Transformer. It is expected -// to be thread-safe. -type Store interface { - // Add adds a transformer to the store with its encrypted key as key. - Add([]byte, Transformer) - // Get returns a transformer from the store by its encrypted key as key. - Get([]byte) (Transformer, bool) -} - -// CreateTransformer enables the creation of a Transformer based on a key. -type CreateTransformer interface { - // Transformer creates a transformer with a given key. - Transformer(context.Context, []byte) (Transformer, error) - // Key creates a key that should match the expectations of Transformer(). - Key() ([]byte, error) -} - -/* -Copied from: - - "k8s.io/apiserver/pkg/storage/value" - - "k8s.io/apiserver/pkg/storage/value/encrypt/aes" -*/ +// 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.