diff --git a/cmd/crypto/error.go b/cmd/crypto/error.go index 1012f5c42..23976ae0e 100644 --- a/cmd/crypto/error.go +++ b/cmd/crypto/error.go @@ -81,8 +81,6 @@ var ( errInvalidInternalIV = Errorf("The internal encryption IV is malformed") errInvalidInternalSealAlgorithm = Errorf("The internal seal algorithm is invalid and not supported") - - errMissingUpdatedKey = Errorf("The key update returned no error but also no sealed key") ) var ( diff --git a/cmd/crypto/kes.go b/cmd/crypto/kes.go index bd03282b4..72ab0d229 100644 --- a/cmd/crypto/kes.go +++ b/cmd/crypto/kes.go @@ -181,7 +181,10 @@ func (kes *kesService) CreateKey(keyID string) error { return kes.client.CreateK // named key referenced by keyID. It also binds the generated key // cryptographically to the provided context. func (kes *kesService) GenerateKey(keyID string, ctx Context) (key [32]byte, sealedKey []byte, err error) { - context := ctx.AppendTo(make([]byte, 0, 128)) + context, err := ctx.MarshalText() + if err != nil { + return key, nil, err + } var plainKey []byte plainKey, sealedKey, err = kes.client.GenerateDataKey(keyID, context) @@ -203,7 +206,10 @@ func (kes *kesService) GenerateKey(keyID string, ctx Context) (key [32]byte, sea // The context must be same context as the one provided while // generating the plaintext key / sealedKey. func (kes *kesService) UnsealKey(keyID string, sealedKey []byte, ctx Context) (key [32]byte, err error) { - context := ctx.AppendTo(make([]byte, 0, 128)) + context, err := ctx.MarshalText() + if err != nil { + return key, err + } var plainKey []byte plainKey, err = kes.client.DecryptDataKey(keyID, sealedKey, context) diff --git a/cmd/crypto/kms.go b/cmd/crypto/kms.go index ebd952b53..273b0b292 100644 --- a/cmd/crypto/kms.go +++ b/cmd/crypto/kms.go @@ -21,7 +21,6 @@ import ( "crypto/rand" "crypto/sha256" "errors" - "fmt" "io" "sort" @@ -33,74 +32,29 @@ import ( // associated with a certain object. type Context map[string]string -// WriteTo writes the context in a canonical from to w. -// It returns the number of bytes and the first error -// encounter during writing to w, if any. -// -// WriteTo sorts the context keys and writes the sorted -// key-value pairs as canonical JSON object to w. -// Sort order is based on the un-escaped keys. -// -// Note that neither keys nor values are escaped for JSON. -func (c Context) WriteTo(w io.Writer) (n int64, err error) { - sortedKeys := make(sort.StringSlice, 0, len(c)) - for k := range c { - sortedKeys = append(sortedKeys, k) - } - sort.Sort(sortedKeys) +// MarshalText returns a canonical text representation of +// the Context. - escape := func(s string) string { - buf := bytes.NewBuffer(make([]byte, 0, len(s))) - EscapeStringJSON(buf, s) - return buf.String() - } - - nn, err := io.WriteString(w, "{") - if err != nil { - return n + int64(nn), err - } - n += int64(nn) - for i, k := range sortedKeys { - s := fmt.Sprintf("\"%s\":\"%s\",", escape(k), escape(c[k])) - if i == len(sortedKeys)-1 { - s = s[:len(s)-1] // remove last ',' - } - - nn, err = io.WriteString(w, s) - if err != nil { - return n + int64(nn), err - } - n += int64(nn) - } - nn, err = io.WriteString(w, "}") - return n + int64(nn), err -} - -// AppendTo appends the context in a canonical from to dst. -// -// AppendTo sorts the context keys and writes the sorted -// key-value pairs as canonical JSON object to w. -// Sort order is based on the un-escaped keys. -// -// Note that neither keys nor values are escaped for JSON. -func (c Context) AppendTo(dst []byte) (output []byte) { +// MarshalText sorts the context keys and writes the sorted +// key-value pairs as canonical JSON object. The sort order +// is based on the un-escaped keys. +func (c Context) MarshalText() ([]byte, error) { if len(c) == 0 { - return append(dst, '{', '}') + return []byte{'{', '}'}, nil } - // out should not escape. - out := bytes.NewBuffer(dst) - - // No need to copy+sort + // Pre-allocate a buffer - 128 bytes is an arbitrary + // heuristic value that seems like a good starting size. + var b = bytes.NewBuffer(make([]byte, 0, 128)) if len(c) == 1 { for k, v := range c { - out.WriteString(`{"`) - EscapeStringJSON(out, k) - out.WriteString(`":"`) - EscapeStringJSON(out, v) - out.WriteString(`"}`) + b.WriteString(`{"`) + EscapeStringJSON(b, k) + b.WriteString(`":"`) + EscapeStringJSON(b, v) + b.WriteString(`"}`) } - return out.Bytes() + return b.Bytes(), nil } sortedKeys := make([]string, 0, len(c)) @@ -109,19 +63,19 @@ func (c Context) AppendTo(dst []byte) (output []byte) { } sort.Strings(sortedKeys) - out.WriteByte('{') + b.WriteByte('{') for i, k := range sortedKeys { - out.WriteByte('"') - EscapeStringJSON(out, k) - out.WriteString(`":"`) - EscapeStringJSON(out, c[k]) - out.WriteByte('"') + b.WriteByte('"') + EscapeStringJSON(b, k) + b.WriteString(`":"`) + EscapeStringJSON(b, c[k]) + b.WriteByte('"') if i < len(sortedKeys)-1 { - out.WriteByte(',') + b.WriteByte(',') } } - out.WriteByte('}') - return out.Bytes() + b.WriteByte('}') + return b.Bytes(), nil } // KMS represents an active and authenticted connection @@ -225,9 +179,11 @@ func (kms *masterKeyKMS) deriveKey(keyID string, context Context) (key [32]byte) if context == nil { context = Context{} } + ctxBytes, _ := context.MarshalText() + mac := hmac.New(sha256.New, kms.masterKey[:]) mac.Write([]byte(keyID)) - mac.Write(context.AppendTo(make([]byte, 0, 128))) + mac.Write(ctxBytes) mac.Sum(key[:0]) return key } diff --git a/cmd/crypto/kms_test.go b/cmd/crypto/kms_test.go index fa6c67f27..7767bc60c 100644 --- a/cmd/crypto/kms_test.go +++ b/cmd/crypto/kms_test.go @@ -18,7 +18,6 @@ import ( "bytes" "fmt" "path" - "strings" "testing" ) @@ -61,7 +60,7 @@ func TestMasterKeyKMS(t *testing.T) { } } -var contextWriteToTests = []struct { +var contextMarshalTextTests = []struct { Context Context ExpectedJSON string }{ @@ -76,43 +75,29 @@ var contextWriteToTests = []struct { 6: {Context: Context{"a": "<>&"}, ExpectedJSON: `{"a":"\u003c\u003e\u0026"}`}, } -func TestContextWriteTo(t *testing.T) { - for i, test := range contextWriteToTests { - var jsonContext strings.Builder - if _, err := test.Context.WriteTo(&jsonContext); err != nil { - t.Errorf("Test %d: Failed to encode context: %v", i, err) - continue +func TestContextMarshalText(t *testing.T) { + for i, test := range contextMarshalTextTests { + text, err := test.Context.MarshalText() + if err != nil { + t.Fatalf("Test %d: Failed to encode context: %v", i, err) } - if s := jsonContext.String(); s != test.ExpectedJSON { - t.Errorf("Test %d: JSON representation differ - got: '%s' want: '%s'", i, s, test.ExpectedJSON) + if string(text) != test.ExpectedJSON { + t.Errorf("Test %d: JSON representation differ - got: '%s' want: '%s'", i, string(text), test.ExpectedJSON) } } } -func TestContextAppendTo(t *testing.T) { - for i, test := range contextWriteToTests { - dst := make([]byte, 0, 1024) - dst = test.Context.AppendTo(dst) - if s := string(dst); s != test.ExpectedJSON { - t.Errorf("Test %d: JSON representation differ - got: '%s' want: '%s'", i, s, test.ExpectedJSON) - } - // Append one more - dst = test.Context.AppendTo(dst) - if s := string(dst); s != test.ExpectedJSON+test.ExpectedJSON { - t.Errorf("Test %d: JSON representation differ - got: '%s' want: '%s'", i, s, test.ExpectedJSON+test.ExpectedJSON) - } - } -} - -func BenchmarkContext_AppendTo(b *testing.B) { +func BenchmarkContext(b *testing.B) { tests := []Context{{}, {"bucket": "warp-benchmark-bucket"}, {"0": "1", "-": "2", ".": "#"}, {"34trg": "dfioutr89", "ikjfdghkjf": "jkedfhgfjkhg", "sdfhsdjkh": "if88889", "asddsirfh804": "kjfdshgdfuhgfg78-45604586#$%<>&"}} for _, test := range tests { b.Run(fmt.Sprintf("%d-elems", len(test)), func(b *testing.B) { - dst := make([]byte, 0, 1024) b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { - dst = test.AppendTo(dst[:0]) + _, err := test.MarshalText() + if err != nil { + b.Fatal(err) + } } }) } diff --git a/cmd/crypto/vault.go b/cmd/crypto/vault.go index 09550eb21..019c8bc1c 100644 --- a/cmd/crypto/vault.go +++ b/cmd/crypto/vault.go @@ -223,7 +223,10 @@ func (v *vaultService) CreateKey(keyID string) error { // named key referenced by keyID. It also binds the generated key // cryptographically to the provided context. func (v *vaultService) GenerateKey(keyID string, ctx Context) (key [32]byte, sealedKey []byte, err error) { - context := ctx.AppendTo(make([]byte, 0, 128)) + context, err := ctx.MarshalText() + if err != nil { + return key, sealedKey, err + } payload := map[string]interface{}{ "context": base64.StdEncoding.EncodeToString(context), @@ -258,7 +261,10 @@ func (v *vaultService) GenerateKey(keyID string, ctx Context) (key [32]byte, sea // The context must be same context as the one provided while // generating the plaintext key / sealedKey. func (v *vaultService) UnsealKey(keyID string, sealedKey []byte, ctx Context) (key [32]byte, err error) { - context := ctx.AppendTo(make([]byte, 0, 128)) + context, err := ctx.MarshalText() + if err != nil { + return key, err + } payload := map[string]interface{}{ "ciphertext": string(sealedKey), @@ -282,29 +288,3 @@ func (v *vaultService) UnsealKey(keyID string, sealedKey []byte, ctx Context) (k copy(key[:], plainKey) return key, nil } - -// UpdateKey re-wraps the sealedKey if the master key referenced by the keyID -// has been changed by the KMS operator - i.e. the master key has been rotated. -// If the master key hasn't changed since the sealedKey has been created / updated -// it may return the same sealedKey as rotatedKey. -// -// The context must be same context as the one provided while -// generating the plaintext key / sealedKey. -func (v *vaultService) UpdateKey(keyID string, sealedKey []byte, ctx Context) (rotatedKey []byte, err error) { - context := ctx.AppendTo(make([]byte, 0, 128)) - - payload := map[string]interface{}{ - "ciphertext": string(sealedKey), - "context": base64.StdEncoding.EncodeToString(context), - } - s, err := v.client.Logical().Write(fmt.Sprintf("/transit/rewrap/%s", keyID), payload) - if err != nil { - return nil, Errorf("crypto: client error %w", err) - } - ciphertext, ok := s.Data["ciphertext"] - if !ok { - return nil, errMissingUpdatedKey - } - rotatedKey = []byte(ciphertext.(string)) - return rotatedKey, nil -}