diff --git a/hack/.linted_packages b/hack/.linted_packages index cda525bb924..ebd78cb5a35 100644 --- a/hack/.linted_packages +++ b/hack/.linted_packages @@ -371,6 +371,7 @@ staging/src/k8s.io/apiserver/pkg/storage/storagebackend/factory staging/src/k8s.io/apiserver/pkg/storage/storagebackend/factory staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/aes staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/identity +staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/secretbox staging/src/k8s.io/apiserver/pkg/util/flushwriter staging/src/k8s.io/apiserver/pkg/util/logs staging/src/k8s.io/apiserver/plugin/pkg/audit/webhook diff --git a/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/secretbox/secretbox.go b/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/secretbox/secretbox.go new file mode 100644 index 00000000000..f53aa2c3701 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/secretbox/secretbox.go @@ -0,0 +1,69 @@ +/* +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 secretbox transforms values for storage at rest using XSalsa20 and Poly1305. +package secretbox + +import ( + "crypto/rand" + "fmt" + + "golang.org/x/crypto/nacl/secretbox" + + "k8s.io/apiserver/pkg/storage/value" +) + +// secretbox implements at rest encryption of the provided values given a 32 byte secret key. +// Uses a standard 24 byte nonce (placed at the the beginning of the cipher text) generated +// from crypto/rand. Does not perform authentication of the data at rest. +type secretboxTransformer struct { + key [32]byte +} + +const nonceSize = 24 + +// NewSecretboxTransformer takes the given key and performs encryption and decryption on the given +// data. +func NewSecretboxTransformer(key [32]byte) value.Transformer { + return &secretboxTransformer{key: key} +} + +func (t *secretboxTransformer) TransformFromStorage(data []byte, context value.Context) ([]byte, bool, error) { + if len(data) < (secretbox.Overhead + nonceSize) { + return nil, false, fmt.Errorf("the stored data was shorter than the required size") + } + var nonce [nonceSize]byte + copy(nonce[:], data[:nonceSize]) + data = data[nonceSize:] + out := make([]byte, 0, len(data)-secretbox.Overhead) + result, ok := secretbox.Open(out, data, &nonce, &t.key) + if !ok { + return nil, false, fmt.Errorf("output array was not large enough for encryption") + } + return result, false, nil +} + +func (t *secretboxTransformer) TransformToStorage(data []byte, context value.Context) ([]byte, error) { + var nonce [nonceSize]byte + n, err := rand.Read(nonce[:]) + if err != nil { + return nil, err + } + if n != nonceSize { + return nil, fmt.Errorf("unable to read sufficient random bytes") + } + return secretbox.Seal(nonce[:], data, &nonce, &t.key), nil +} diff --git a/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/secretbox/secretbox_test.go b/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/secretbox/secretbox_test.go new file mode 100644 index 00000000000..80807574a54 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/storage/value/encrypt/secretbox/secretbox_test.go @@ -0,0 +1,190 @@ +/* +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 secretbox + +import ( + "bytes" + "crypto/rand" + "encoding/hex" + "fmt" + "io" + "reflect" + "testing" + + "k8s.io/apiserver/pkg/storage/value" +) + +var ( + key1 = [32]byte{0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01} + key2 = [32]byte{0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02} +) + +func TestSecretboxKeyRotation(t *testing.T) { + testErr := fmt.Errorf("test error") + context := value.DefaultContext([]byte("authenticated_data")) + + p := value.NewPrefixTransformers(testErr, + value.PrefixTransformer{Prefix: []byte("first:"), Transformer: NewSecretboxTransformer(key1)}, + value.PrefixTransformer{Prefix: []byte("second:"), Transformer: NewSecretboxTransformer(key2)}, + ) + 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 does not fails storage + // Secretbox is not currently an authenticating store + from, stale, err = p.TransformFromStorage(out, value.DefaultContext([]byte("incorrect_context"))) + if err != nil { + t.Fatalf("secretbox is not authenticated") + } + + // reverse the order, use the second key + p = value.NewPrefixTransformers(testErr, + value.PrefixTransformer{Prefix: []byte("second:"), Transformer: NewSecretboxTransformer(key2)}, + value.PrefixTransformer{Prefix: []byte("first:"), Transformer: NewSecretboxTransformer(key1)}, + ) + 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 BenchmarkSecretboxRead_32_1024(b *testing.B) { benchmarkSecretboxRead(b, 32, 1024, false) } +func BenchmarkSecretboxRead_32_16384(b *testing.B) { benchmarkSecretboxRead(b, 32, 16384, false) } +func BenchmarkSecretboxRead_32_16384_Stale(b *testing.B) { benchmarkSecretboxRead(b, 32, 16384, true) } + +func BenchmarkSecretboxWrite_32_1024(b *testing.B) { benchmarkSecretboxWrite(b, 32, 1024) } +func BenchmarkSecretboxWrite_32_16384(b *testing.B) { benchmarkSecretboxWrite(b, 32, 16384) } + +func benchmarkSecretboxRead(b *testing.B, keyLength int, valueLength int, stale bool) { + p := value.NewPrefixTransformers(nil, + value.PrefixTransformer{Prefix: []byte("first:"), Transformer: NewSecretboxTransformer(key1)}, + value.PrefixTransformer{Prefix: []byte("second:"), Transformer: NewSecretboxTransformer(key2)}, + ) + + 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: NewSecretboxTransformer(key1)}, + value.PrefixTransformer{Prefix: []byte("second:"), Transformer: NewSecretboxTransformer(key2)}, + ) + } + + 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 benchmarkSecretboxWrite(b *testing.B, keyLength int, valueLength int) { + p := value.NewPrefixTransformers(nil, + value.PrefixTransformer{Prefix: []byte("first:"), Transformer: NewSecretboxTransformer(key1)}, + value.PrefixTransformer{Prefix: []byte("second:"), Transformer: NewSecretboxTransformer(key2)}, + ) + + 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} + + tests := []struct { + name string + context value.Context + t value.Transformer + }{ + {name: "GCM 16 byte key", t: NewSecretboxTransformer(key1)}, + } + 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)) + } + } + }) + } +}