diff --git a/cmd/crypto/kes.go b/cmd/crypto/kes.go index 5c77bf216..10f3c0b8d 100644 --- a/cmd/crypto/kes.go +++ b/cmd/crypto/kes.go @@ -34,6 +34,10 @@ import ( xnet "github.com/minio/minio/pkg/net" ) +// ErrKESKeyNotFound is the error returned a KES server +// when a master key does not exist. +var ErrKESKeyNotFound = NewKESError(http.StatusNotFound, "key does not exist") + // KesConfig contains the configuration required // to initialize and connect to a kes server. type KesConfig struct { @@ -154,6 +158,12 @@ func (kes *kesService) GenerateKey(keyID string, ctx Context) (key [32]byte, sea var plainKey []byte plainKey, sealedKey, err = kes.client.GenerateDataKey(keyID, context.Bytes()) + if err == ErrKESKeyNotFound { // Try to create the key if it does not exist. + if err = kes.client.CreateKey(keyID); err != nil { + return key, nil, err + } + plainKey, sealedKey, err = kes.client.GenerateDataKey(keyID, context.Bytes()) + } if err != nil { return key, nil, err } @@ -214,16 +224,19 @@ type kesClient struct { httpClient http.Client } -// Response KES response struct -type response struct { - Plaintext []byte `json:"plaintext"` - Ciphertext []byte `json:"ciphertext,omitempty"` -} - -// Request KES request struct -type request struct { - Ciphertext []byte `json:"ciphertext,omitempty"` - Context []byte `json:"context"` +// CreateKey tries to create a new cryptographic key with +// the specified name. +// +// The key will be generated by the server. The client +// application does not have the cryptographic key at +// any point in time. +func (c *kesClient) CreateKey(name string) error { + url := fmt.Sprintf("%s/v1/key/create/%s", c.addr, url.PathEscape(name)) + _, err := c.postRetry(url, nil, 0) // No request body and no response expected + if err != nil { + return err + } + return nil } // GenerateDataKey requests a new data key from the KES server. @@ -235,48 +248,155 @@ type request struct { // such that you have to provide the same context when decrypting // the data key. func (c *kesClient) GenerateDataKey(name string, context []byte) ([]byte, []byte, error) { - body, err := json.Marshal(request{ + type Request struct { + Context []byte `json:"context"` + } + type Response struct { + Plaintext []byte `json:"plaintext"` + Ciphertext []byte `json:"ciphertext"` + } + + body, err := json.Marshal(Request{ Context: context, }) if err != nil { return nil, nil, err } - url := fmt.Sprintf("%s/v1/key/generate/%s", c.addr, url.PathEscape(name)) - const limit = 1 << 20 // A plaintext/ciphertext key pair will never be larger than 1 MB + url := fmt.Sprintf("%s/v1/key/generate/%s", c.addr, url.PathEscape(name)) resp, err := c.postRetry(url, bytes.NewReader(body), limit) if err != nil { return nil, nil, err } - return resp.Plaintext, resp.Ciphertext, nil + var response Response + if err = json.NewDecoder(resp).Decode(&response); err != nil { + return nil, nil, err + } + return response.Plaintext, response.Ciphertext, nil } -func (c *kesClient) post(url string, body io.Reader, limit int64) (*response, error) { - resp, err := c.httpClient.Post(url, "application/json", body) +// GenerateDataKey decrypts an encrypted data key with the key +// specified by name by talking to the KES server. +// On success, the KES server will respond with the plaintext key. +// +// The optional context must match the value you provided when +// generating the data key. +func (c *kesClient) DecryptDataKey(name string, ciphertext, context []byte) ([]byte, error) { + type Request struct { + Ciphertext []byte `json:"ciphertext"` + Context []byte `json:"context,omitempty"` + } + type Response struct { + Plaintext []byte `json:"plaintext"` + } + + body, err := json.Marshal(Request{ + Ciphertext: ciphertext, + Context: context, + }) if err != nil { return nil, err } + const limit = 1 << 20 // A data key will never be larger than 1 MiB + url := fmt.Sprintf("%s/v1/key/decrypt/%s", c.addr, url.PathEscape(name)) + resp, err := c.postRetry(url, bytes.NewReader(body), limit) + if err != nil { + return nil, err + } + + var response Response + if err = json.NewDecoder(resp).Decode(&response); err != nil { + return nil, err + } + return response.Plaintext, nil +} + +// NewKESError returns a new KES API error with the given +// HTTP status code and error message. +// +// Two errors with the same status code and +// error message are equal: +// e1 == e2 // true. +func NewKESError(code int, text string) error { + return kesError{ + code: code, + message: text, + } +} + +type kesError struct { + code int + message string +} + +// Status returns the HTTP status code of the error. +func (e kesError) Status() int { return e.code } + +// Status returns the error message of the error. +func (e kesError) Error() string { return e.message } + +func parseErrorResponse(resp *http.Response) error { + if resp == nil || resp.StatusCode < 400 { + return nil + } + if resp.Body == nil { + return NewKESError(resp.StatusCode, "") + } + defer resp.Body.Close() + + const MaxBodySize = 1 << 20 + var size = resp.ContentLength + if size < 0 || size > MaxBodySize { + size = MaxBodySize + } + + contentType := strings.TrimSpace(resp.Header.Get("Content-Type")) + if strings.HasPrefix(contentType, "application/json") { + type Response struct { + Message string `json:"message"` + } + var response Response + if err := json.NewDecoder(io.LimitReader(resp.Body, size)).Decode(&response); err != nil { + return err + } + return NewKESError(resp.StatusCode, response.Message) + } + + var sb strings.Builder + if _, err := io.Copy(&sb, io.LimitReader(resp.Body, size)); err != nil { + return err + } + return NewKESError(resp.StatusCode, sb.String()) +} + +func (c *kesClient) post(url string, body io.Reader, limit int64) (io.Reader, error) { + resp, err := c.httpClient.Post(url, "application/json", body) + if err != nil { + return nil, err + } // Drain the entire body to make sure we have re-use connections defer xhttp.DrainBody(resp.Body) if resp.StatusCode != http.StatusOK { - return nil, c.parseErrorResponse(resp) + return nil, parseErrorResponse(resp) } - response := &response{} - if err = json.NewDecoder(io.LimitReader(resp.Body, limit)).Decode(response); err != nil { + // We have to copy the response body due to draining. + var respBody bytes.Buffer + if _, err = io.Copy(&respBody, io.LimitReader(resp.Body, limit)); err != nil { return nil, err } - return response, nil + return &respBody, nil } -func (c *kesClient) postRetry(url string, body io.ReadSeeker, limit int64) (*response, error) { +func (c *kesClient) postRetry(url string, body io.ReadSeeker, limit int64) (io.Reader, error) { for i := 0; ; i++ { - body.Seek(0, io.SeekStart) // seek to the beginning of the body. - + if body != nil { + body.Seek(0, io.SeekStart) // seek to the beginning of the body. + } response, err := c.post(url, body, limit) if err == nil { return response, nil @@ -296,45 +416,6 @@ func (c *kesClient) postRetry(url string, body io.ReadSeeker, limit int64) (*res } } -// GenerateDataKey decrypts an encrypted data key with the key -// specified by name by talking to the KES server. -// On success, the KES server will respond with the plaintext key. -// -// The optional context must match the value you provided when -// generating the data key. -func (c *kesClient) DecryptDataKey(name string, ciphertext, context []byte) ([]byte, error) { - body, err := json.Marshal(request{ - Ciphertext: ciphertext, - Context: context, - }) - if err != nil { - return nil, err - } - - url := fmt.Sprintf("%s/v1/key/decrypt/%s", c.addr, url.PathEscape(name)) - - const limit = 32 * 1024 // A data key will never be larger than 32 KB - resp, err := c.postRetry(url, bytes.NewReader(body), limit) - if err != nil { - return nil, err - } - return resp.Plaintext, nil -} - -func (c *kesClient) parseErrorResponse(resp *http.Response) error { - if resp.Body == nil { - return Errorf("%s: no body", http.StatusText(resp.StatusCode)) - } - - const limit = 32 * 1024 // A (valid) error response will not be greater than 32 KB - var errMsg strings.Builder - if _, err := io.Copy(&errMsg, io.LimitReader(resp.Body, limit)); err != nil { - return Errorf("%s: %s", http.StatusText(resp.StatusCode), err) - } - - return Errorf("%s: %s", http.StatusText(resp.StatusCode), errMsg.String()) -} - // loadCACertificates returns a new CertPool // that contains all system root CA certificates // and any PEM-encoded certificate(s) found at