storage: add HMACKey management

Adds methods:
 (*Client) CreateHMACKey(context.Context, projectID, serviceAccount)
 (*Client) HMACKeyHandle(projectID, accessKeyId)
 (*HMACKeyHandle) Delete(context.Context) error
 (*HMACKeyHandle) Get(context.Context) (*HMACKey, error)
 (*HMACKeyHandle) Update(context.Context) (*HMACKey, error)

An HMACKey will be commonly referred to
by an accessId such as:

    GOOGTS7C7FUP3AIRVJTE2BCD

while a Secret will look for example like:

    bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ

One can enable "HMACKey interoperability" by visiting
https://cloud.google.com/storage/docs/migrating

Another CL will follow up to add Listing capabilities.

Updates #1486

Change-Id: I2e5e5d597265eb5b747bed250695d7ddd5df4dad
Reviewed-on: https://code-review.googlesource.com/c/gocloud/+/42550
Reviewed-by: kokoro <noreply+kokoro@google.com>
Reviewed-by: Jonathan Lui <jonathanlui@google.com>
Reviewed-by: Frank Natividad <franknatividad@google.com>
Reviewed-by: Jean de Klerk <deklerk@google.com>
diff --git a/storage/example_test.go b/storage/example_test.go
index 5162f95..235130d 100644
--- a/storage/example_test.go
+++ b/storage/example_test.go
@@ -663,3 +663,63 @@
 		// TODO: handle error.
 	}
 }
+
+func ExampleClient_CreateHMACKey() {
+	ctx := context.Background()
+	client, err := storage.NewClient(ctx)
+	if err != nil {
+		// TODO: handle error.
+	}
+
+	hkey, err := client.CreateHMACKey(ctx, "project-id", "service-account-email")
+	if err != nil {
+		// TODO: handle error.
+	}
+	_ = hkey // TODO: Use the HMAC Key.
+}
+
+func ExampleHMACKeyHandle_Delete() {
+	ctx := context.Background()
+	client, err := storage.NewClient(ctx)
+	if err != nil {
+		// TODO: handle error.
+	}
+
+	hkh := client.HMACKeyHandle("project-id", "access-key-id")
+	// Make sure that the HMACKey being deleted has a status of inactive.
+	if err := hkh.Delete(ctx); err != nil {
+		// TODO: handle error.
+	}
+}
+
+func ExampleHMACKeyHandle_Get() {
+	ctx := context.Background()
+	client, err := storage.NewClient(ctx)
+	if err != nil {
+		// TODO: handle error.
+	}
+
+	hkh := client.HMACKeyHandle("project-id", "access-key-id")
+	hkey, err := hkh.Get(ctx)
+	if err != nil {
+		// TODO: handle error.
+	}
+	_ = hkey // TODO: Use the HMAC Key.
+}
+
+func ExampleHMACKeyHandle_Update() {
+	ctx := context.Background()
+	client, err := storage.NewClient(ctx)
+	if err != nil {
+		// TODO: handle error.
+	}
+
+	hkh := client.HMACKeyHandle("project-id", "access-key-id")
+	ukey, err := hkh.Update(ctx, storage.HMACKeyAttrsToUpdate{
+		State: storage.Inactive,
+	})
+	if err != nil {
+		// TODO: handle error.
+	}
+	_ = ukey // TODO: Use the HMAC Key.
+}
diff --git a/storage/hmac.go b/storage/hmac.go
new file mode 100644
index 0000000..a906d44
--- /dev/null
+++ b/storage/hmac.go
@@ -0,0 +1,227 @@
+// Copyright 2019 Google LLC
+//
+// 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 storage
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"time"
+
+	raw "google.golang.org/api/storage/v1"
+)
+
+// HMACState is the state of the HMAC key.
+type HMACState string
+
+const (
+	// Active is the status for an active key that can be used to sign
+	// requests.
+	Active HMACState = "ACTIVE"
+
+	// Inactive is the status for an inactive key thus requests signed by
+	// this key will be denied.
+	Inactive HMACState = "INACTIVE"
+
+	// Deleted is the status for a key that is deleted.
+	// Once in this state the key cannot key cannot be recovered
+	// and does not count towards key limits. Deleted keys will be cleaned
+	// up later.
+	Deleted HMACState = "DELETED"
+)
+
+// HMACKey is the representation of a Google Cloud Storage HMAC key.
+//
+// HMAC keys are used to authenticate signed access to objects. To enable HMAC key
+// authentication, please visit https://cloud.google.com/storage/docs/migrating.
+//
+// This type is experimental and subject to change.
+type HMACKey struct {
+	// The HMAC's secret key.
+	Secret string
+
+	// AccessID is the ID of the HMAC key.
+	AccessID string
+
+	// Etag is the HTTP/1.1 Entity tag.
+	Etag string
+
+	// ID is the ID of the HMAC key, including the ProjectID and AccessID.
+	ID string
+
+	// ProjectID is the ID of the project that owns the
+	// service account to which the key authenticates.
+	ProjectID string
+
+	// ServiceAccountEmail is the email address
+	// of the key's associated service account.
+	ServiceAccountEmail string
+
+	// CreatedTime is the creation time of the HMAC key.
+	CreatedTime time.Time
+
+	// UpdatedTime is the last modification time of the HMAC key metadata.
+	UpdatedTime time.Time
+
+	// State is the state of the HMAC key.
+	// It can be one of StateActive, StateInactive or StateDeleted.
+	State HMACState
+}
+
+// HMACKeyHandle helps provide access and management for HMAC keys.
+//
+// This type is experimental and subject to change.
+type HMACKeyHandle struct {
+	projectID string
+	accessID  string
+
+	raw *raw.ProjectsHmacKeysService
+}
+
+// HMACKeyHandle creates a handle that will be used for HMACKey operations.
+func (c *Client) HMACKeyHandle(projectID, accessID string) *HMACKeyHandle {
+	return &HMACKeyHandle{
+		projectID: projectID,
+		accessID:  accessID,
+		raw:       raw.NewProjectsHmacKeysService(c.raw),
+	}
+}
+
+// Get invokes an RPC to retrieve the HMAC key referenced by the
+// HMACKeyHandle's accessID.
+func (hkh *HMACKeyHandle) Get(ctx context.Context) (*HMACKey, error) {
+	call := hkh.raw.Get(hkh.projectID, hkh.accessID)
+	setClientHeader(call.Header())
+
+	var metadata *raw.HmacKeyMetadata
+	var err error
+	err = runWithRetry(ctx, func() error {
+		metadata, err = call.Context(ctx).Do()
+		return err
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	hkPb := &raw.HmacKey{
+		Metadata: metadata,
+	}
+	return pbHmacKeyToHMACKey(hkPb, false)
+}
+
+// Delete invokes an RPC to delete the key referenced by accessID, on Google Cloud Storage.
+// Only inactive HMAC keys can be deleted.
+// After deletion, a key cannot be used to authenticate requests.
+func (hkh *HMACKeyHandle) Delete(ctx context.Context) error {
+	delCall := hkh.raw.Delete(hkh.projectID, hkh.accessID)
+	setClientHeader(delCall.Header())
+
+	return runWithRetry(ctx, func() error {
+		return delCall.Context(ctx).Do()
+	})
+}
+
+func pbHmacKeyToHMACKey(pb *raw.HmacKey, isCreate bool) (*HMACKey, error) {
+	pbmd := pb.Metadata
+	if pbmd == nil {
+		return nil, errors.New("field Metadata cannot be nil")
+	}
+	createdTime, err := time.Parse(time.RFC3339, pbmd.TimeCreated)
+	if err != nil {
+		return nil, fmt.Errorf("field CreatedTime: %v", err)
+	}
+	updatedTime, err := time.Parse(time.RFC3339, pbmd.Updated)
+	if err != nil && !isCreate {
+		return nil, fmt.Errorf("field UpdatedTime: %v", err)
+	}
+
+	hmk := &HMACKey{
+		AccessID:    pbmd.AccessId,
+		Secret:      pb.Secret,
+		Etag:        pbmd.Etag,
+		ID:          pbmd.Id,
+		State:       HMACState(pbmd.State),
+		ProjectID:   pbmd.ProjectId,
+		CreatedTime: createdTime,
+		UpdatedTime: updatedTime,
+
+		ServiceAccountEmail: pbmd.ServiceAccountEmail,
+	}
+
+	return hmk, nil
+}
+
+// CreateHMACKey invokes an RPC for Google Cloud Storage to create a new HMACKey.
+func (c *Client) CreateHMACKey(ctx context.Context, projectID, serviceAccountEmail string) (*HMACKey, error) {
+	if projectID == "" {
+		return nil, errors.New("storage: expecting a non-blank projectID")
+	}
+	if serviceAccountEmail == "" {
+		return nil, errors.New("storage: expecting a non-blank service account email")
+	}
+
+	svc := raw.NewProjectsHmacKeysService(c.raw)
+	call := svc.Create(projectID, serviceAccountEmail)
+	setClientHeader(call.Header())
+
+	var hkPb *raw.HmacKey
+	var err error
+	err = runWithRetry(ctx, func() error {
+		hkPb, err = call.Context(ctx).Do()
+		return err
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	return pbHmacKeyToHMACKey(hkPb, true)
+}
+
+// HMACKeyAttrsToUpdate defines the attributes of an HMACKey that will be updated.
+type HMACKeyAttrsToUpdate struct {
+	// State is required and must be either StateActive or StateInactive.
+	State HMACState
+
+	// Etag is an optional field and it is the HTTP/1.1 Entity tag.
+	Etag string
+}
+
+// Update mutates the HMACKey referred to by accessID.
+func (h *HMACKeyHandle) Update(ctx context.Context, au HMACKeyAttrsToUpdate) (*HMACKey, error) {
+	if au.State != Active && au.State != Inactive {
+		return nil, fmt.Errorf("storage: invalid state %q for update, must be either %q or %q", au.State, Active, Inactive)
+	}
+
+	call := h.raw.Update(h.projectID, h.accessID, &raw.HmacKeyMetadata{
+		Etag:  au.Etag,
+		State: string(au.State),
+	})
+	setClientHeader(call.Header())
+
+	var metadata *raw.HmacKeyMetadata
+	var err error
+	err = runWithRetry(ctx, func() error {
+		metadata, err = call.Context(ctx).Do()
+		return err
+	})
+
+	if err != nil {
+		return nil, err
+	}
+	hkPb := &raw.HmacKey{
+		Metadata: metadata,
+	}
+	return pbHmacKeyToHMACKey(hkPb, false)
+}
diff --git a/storage/hmac_test.go b/storage/hmac_test.go
new file mode 100644
index 0000000..b53651f
--- /dev/null
+++ b/storage/hmac_test.go
@@ -0,0 +1,380 @@
+// Copyright 2019 Google LLC
+//
+// 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 storage
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"strings"
+	"testing"
+	"time"
+
+	"cloud.google.com/go/internal/testutil"
+	"google.golang.org/api/googleapi"
+)
+
+func TestHMACKeyHandle_GetParsing(t *testing.T) {
+	mt := &mockTransport{}
+	client := mockClient(t, mt)
+	projectID := "hmackey-project-id"
+	ctx := context.Background()
+
+	tests := []struct {
+		res     string
+		want    *HMACKey
+		wantErr string
+	}{
+		{
+			res: fmt.Sprintf(`
+                            {
+                                "kind": "storage#hmacKeyMetadata",
+                                "projectId":%q,"state":"ACTIVE",
+                                "timeCreated": "2019-07-06T11:21:58+00:00",
+                                "updated": "2019-07-06T11:22:18+00:00"
+                            }`, projectID),
+			want: &HMACKey{
+				State:       Active,
+				ProjectID:   projectID,
+				UpdatedTime: time.Date(2019, 07, 06, 11, 22, 18, 0, time.UTC),
+				CreatedTime: time.Date(2019, 07, 06, 11, 21, 58, 0, time.UTC),
+			},
+		},
+		{
+			res: fmt.Sprintf(`
+                            {
+                                "kind": "storage#hmacKeyMetadata",
+                                "projectId":%q,"state":"ACTIVE",
+                                "timeCreated": "2019-07-06T11:21:58+00:00",
+                                "updated": "2019-07-06T11:22:18+00:00"
+                            }`, projectID),
+			want: &HMACKey{
+				State:       Active,
+				ProjectID:   projectID,
+				UpdatedTime: time.Date(2019, 07, 06, 11, 22, 18, 0, time.UTC),
+				CreatedTime: time.Date(2019, 07, 06, 11, 21, 58, 0, time.UTC),
+			},
+		},
+		{
+			res: `{}`,
+			// CreatedTime must be formatted in RFC 3339.
+			wantErr: `CreatedTime: parsing time "" as "2006-01-02T15:04:05Z07:00"`,
+		},
+		{
+			res: `{"timeCreated": "2019-07-foo"}`,
+			// CreatedTime must be formatted in RFC 3339.
+			wantErr: `CreatedTime: parsing time "2019-07-foo" as "2006-01-02T15:04:05Z07:00"`,
+		},
+		{
+			res: `{
+                                "kind": "storage#hmacKeyMetadata",
+                                "state":"INACTIVE",
+                                "timeCreated": "2019-07-06T11:21:58+00:00"
+                            }`,
+			// UpdatedTime must be formatted in RFC 3339.
+			wantErr: `UpdatedTime: parsing time "" as "2006-01-02T15:04:05Z07:00"`,
+		},
+	}
+
+	for i, tt := range tests {
+		mt.addResult(&http.Response{
+			ProtoMajor:    1,
+			ProtoMinor:    1,
+			ContentLength: int64(len(tt.res)),
+			Status:        "OK",
+			StatusCode:    200,
+			Body:          bodyReader(tt.res),
+		}, nil)
+		hkh := client.HMACKeyHandle(projectID, "some-access-key-id")
+		got, err := hkh.Get(ctx)
+		if tt.wantErr != "" {
+			if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
+				t.Errorf("#%d: failed to match errors:\ngot:  %q\nwant: %q", i, err, tt.wantErr)
+			}
+			if got != nil {
+				t.Errorf("#%d: unexpectedly got a non-nil result: %#v\n", i, got)
+			}
+			continue
+		}
+
+		if err != nil {
+			t.Errorf("#%d: got an unexpected error: %v", i, err)
+			continue
+		}
+
+		if diff := testutil.Diff(got, tt.want); diff != "" {
+			t.Errorf("#%d: got - want +\n\n%s", i, diff)
+		}
+	}
+}
+
+func TestHMACKeyHandle_Get_NotFound(t *testing.T) {
+	mt := &mockTransport{}
+	client := mockClient(t, mt)
+	ctx := context.Background()
+
+	mt.addResult(&http.Response{
+		ProtoMajor: 2,
+		ProtoMinor: 0,
+		Status:     "OK",
+		StatusCode: http.StatusNotFound,
+		Body:       bodyReader("Access ID not found in project"),
+	}, nil)
+
+	hkh := client.HMACKeyHandle("project-id", "some-access-key-id")
+	_, gotErr := hkh.Get(ctx)
+
+	wantErr := &googleapi.Error{
+		Body:    "Access ID not found in project",
+		Code:    http.StatusNotFound,
+		Message: "",
+	}
+	if diff := testutil.Diff(gotErr, wantErr); diff != "" {
+		t.Fatalf("Error mismatch, got - want +\n%s", diff)
+	}
+}
+
+func TestHMACKeyHandle_Delete(t *testing.T) {
+	mt := &mockTransport{}
+	client := mockClient(t, mt)
+	ctx := context.Background()
+
+	tests := []struct {
+		statusCode int
+		msg        string
+		wantErr    error
+	}{
+		{
+			statusCode: http.StatusBadRequest,
+			msg:        "Cannot delete keys in 'ACTIVE' state",
+			wantErr: &googleapi.Error{
+				Code: http.StatusBadRequest, Message: "Cannot delete keys in 'ACTIVE' state",
+				Body: `{"error":{"message":"Cannot delete keys in 'ACTIVE' state"}}`,
+			},
+		},
+		{
+			statusCode: http.StatusNotFound,
+			msg:        "random message",
+			wantErr: &googleapi.Error{
+				Code: http.StatusNotFound, Message: "random message",
+				Body: `{"error":{"message":"random message"}}`,
+			},
+		},
+		{
+			statusCode: http.StatusNotFound,
+			msg:        "Access ID not found in project",
+			wantErr: &googleapi.Error{
+				Code: http.StatusNotFound, Message: "Access ID not found in project",
+				Body: `{"error":{"message":"Access ID not found in project"}}`,
+			},
+		},
+	}
+
+	for i, tt := range tests {
+		mt.addResult(&http.Response{
+			ProtoMajor: 2,
+			ProtoMinor: 0,
+			Status:     tt.msg,
+			StatusCode: tt.statusCode,
+			Body:       bodyReader(fmt.Sprintf(`{"error":{"message":%q}}`, tt.msg)),
+		}, nil)
+
+		hkh := client.HMACKeyHandle("project", "access-key-id")
+		err := hkh.Delete(ctx)
+
+		if diff := testutil.Diff(err, tt.wantErr); diff != "" {
+			t.Errorf("#%d: error mismatch got - want +\n%s", i, diff)
+		}
+	}
+}
+
+func TestHMACKeyHandle_Create(t *testing.T) {
+	mt := &mockTransport{}
+	client := mockClient(t, mt)
+	projectID := "hmackey-project-id"
+	serviceAccountEmail := "service-account-email-1"
+	ctx := context.Background()
+
+	tests := []struct {
+		res     string
+		want    *HMACKey
+		wantErr string
+	}{
+		{
+			res: `
+                            {
+                                "kind": "storage#hmackey",
+				"secret":"bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ",
+                                "metadata": {
+                                    "projectId":"project-id","state":"ACTIVE",
+                                    "timeCreated": "2019-07-06T11:21:58+00:00",
+                                    "updated": "2019-07-06T11:22:18+00:00"
+                                }
+                            }`,
+			want: &HMACKey{
+				Secret:      "bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ",
+				State:       Active,
+				ProjectID:   "project-id",
+				UpdatedTime: time.Date(2019, 07, 06, 11, 22, 18, 0, time.UTC),
+				CreatedTime: time.Date(2019, 07, 06, 11, 21, 58, 0, time.UTC),
+			},
+		},
+		{
+			res:     `{}`,
+			wantErr: "Metadata cannot be nil",
+		},
+		{
+			res: `{"metadata":{}}`,
+			// CreatedTime must be non-empty and it must formatted in RFC 3339.
+			wantErr: `CreatedTime: parsing time "" as "2006-01-02T15:04:05Z07:00"`,
+		},
+		{
+			res: `{"metadata":{"timeCreated": "2019-07-foo"}}`,
+			// CreatedTime must be formatted in RFC 3339.
+			wantErr: `CreatedTime: parsing time "2019-07-foo" as "2006-01-02T15:04:05Z07:00"`,
+		},
+		{
+			res: `{
+                                "kind": "storage#hmackey",
+				"secret":"bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ",
+                                "metadata":{
+                                    "kind": "storage#hmacKeyMetadata",
+                                    "state":"ACTIVE",
+                                    "timeCreated": "2019-07-06T12:11:33+00:00",
+                                    "projectId": "project-id",
+                                    "updated": ""
+                                }
+                            }`,
+			// ONLY during creation is it okay for UpdatedTime to not be set.
+			want: &HMACKey{
+				Secret:      "bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ",
+				State:       Active,
+				ProjectID:   "project-id",
+				CreatedTime: time.Date(2019, 07, 06, 12, 11, 33, 0, time.UTC),
+			},
+		},
+	}
+
+	for i, tt := range tests {
+		mt.addResult(&http.Response{
+			ProtoMajor:    1,
+			ProtoMinor:    1,
+			ContentLength: int64(len(tt.res)),
+			Status:        "OK",
+			StatusCode:    200,
+			Body:          bodyReader(tt.res),
+		}, nil)
+		got, err := client.CreateHMACKey(ctx, projectID, serviceAccountEmail)
+		if tt.wantErr != "" {
+			if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
+				t.Errorf("#%d: failed to match errors:\ngot:  %q\nwant: %q", i, err, tt.wantErr)
+			}
+			if got != nil {
+				t.Errorf("#%d: unexpectedly got a non-nil result: %#v\n", i, got)
+			}
+			continue
+		}
+
+		if err != nil {
+			t.Errorf("#%d: got an unexpected error: %v", i, err)
+			continue
+		}
+
+		if diff := testutil.Diff(got, tt.want); diff != "" {
+			t.Errorf("#%d: got - want +\n\n%s", i, diff)
+		}
+	}
+
+	// Lastly ensure that a blank service account will return an error.
+	mt.addResult(&http.Response{
+		ProtoMajor: 1,
+		ProtoMinor: 1,
+		Status:     "OK",
+		StatusCode: 200,
+		Body:       bodyReader("{}"),
+	}, nil)
+	hk, err := client.CreateHMACKey(ctx, projectID, "")
+	if err == nil {
+		t.Fatal("Unexpectedly succeeded in creating a key using a blank service account email")
+	}
+	if !strings.Contains(err.Error(), "non-blank service account email") {
+		t.Fatalf("Expected an error about a non-blank service account email: %v", err)
+	}
+	if hk != nil {
+		t.Fatalf("Unexpectedly got back a created HMACKey: %#v", hk)
+	}
+}
+
+func TestHMACKey_UpdateState(t *testing.T) {
+	// This test ensures that updating the state can only
+	// happen with either of Active or Inactive.
+
+	mt := &mockTransport{}
+	client := mockClient(t, mt)
+	projectID := "hmackey-project-id"
+	ctx := context.Background()
+
+	hkh := client.HMACKeyHandle(projectID, "some-access-id")
+
+	// 1. Ensure that invalid states are NOT accepted for an Update.
+	invalidStates := []HMACState{"", Deleted, "active", "inactive", "foo_bar"}
+	for _, invalidState := range invalidStates {
+		t.Run("invalid-"+string(invalidState), func(t *testing.T) {
+			_, err := hkh.Update(ctx, HMACKeyAttrsToUpdate{
+				State: invalidState,
+			})
+			if err == nil {
+				t.Fatal("Unexpectedly succeeded")
+			}
+			invalidStateMsg := fmt.Sprintf(`storage: invalid state %q for update, must be either "ACTIVE" or "INACTIVE"`, invalidState)
+			if err.Error() != invalidStateMsg {
+				t.Fatalf("Mismatched error: got:  %q\nwant: %q", err, invalidStateMsg)
+			}
+		})
+	}
+
+	// 2. Ensure that valid states for Update are accepted.
+	validStates := []HMACState{Active, Inactive}
+	for _, validState := range validStates {
+		t.Run("valid-"+string(validState), func(t *testing.T) {
+			resBody := fmt.Sprintf(`{
+                                    "kind": "storage#hmacKeyMetadata",
+                                    "state":%q,
+                                    "timeCreated": "2019-07-11T12:11:33+00:00",
+                                    "projectId": "project-id",
+                                    "updated": "2019-07-11T12:13:33+00:00"
+                                }
+                            }`, validState)
+			mt.addResult(&http.Response{
+				ProtoMajor:    1,
+				ProtoMinor:    1,
+				ContentLength: int64(len(resBody)),
+				Status:        "OK",
+				StatusCode:    200,
+				Body:          bodyReader(resBody),
+			}, nil)
+
+			hu, err := hkh.Update(ctx, HMACKeyAttrsToUpdate{
+				State: validState,
+			})
+			if err != nil {
+				t.Fatalf("Unexpected failure: %v", err)
+			}
+			if hu.State != validState {
+				t.Fatalf("Unexpected updated state %q, expected %q", hu.State, validState)
+			}
+		})
+	}
+}
diff --git a/storage/integration_test.go b/storage/integration_test.go
index 1b25041..e4a92bc 100644
--- a/storage/integration_test.go
+++ b/storage/integration_test.go
@@ -2760,6 +2760,94 @@
 	}
 }
 
+func TestIntegration_HMACKey_Update(t *testing.T) {
+	ctx := context.Background()
+	client := testConfig(ctx, t)
+	defer client.Close()
+
+	projectID := testutil.ProjID()
+	serviceAccountEmail, err := client.ServiceAccount(ctx, projectID)
+	if err != nil {
+		t.Fatalf("Failed to get service account: %v", err)
+	}
+	hmacKey, err := client.CreateHMACKey(ctx, projectID, serviceAccountEmail)
+	if err != nil {
+		t.Fatalf("Failed to create HMACKey: %v", err)
+	}
+	if hmacKey == nil {
+		t.Fatal("Unexpectedly got back a nil HMAC key")
+	}
+
+	if hmacKey.State != Active {
+		t.Fatalf("Unexpected state %q, expected %q", hmacKey.State, Active)
+	}
+
+	hkh := client.HMACKeyHandle(projectID, hmacKey.AccessID)
+	// 1. Ensure that we CANNOT delete an ACTIVE key.
+	if err := hkh.Delete(ctx); err == nil {
+		t.Fatalf("Unexpectedly deleted key whose state is ACTIVE: %v", err)
+	}
+
+	invalidStates := []HMACState{"", Deleted, "active", "inactive", "foo_bar"}
+	for _, invalidState := range invalidStates {
+		t.Run("invalid-"+string(invalidState), func(t *testing.T) {
+			_, err := hkh.Update(ctx, HMACKeyAttrsToUpdate{
+				State: invalidState,
+			})
+			if err == nil {
+				t.Fatal("Unexpectedly succeeded")
+			}
+			invalidStateMsg := fmt.Sprintf(`storage: invalid state %q for update, must be either "ACTIVE" or "INACTIVE"`, invalidState)
+			if err.Error() != invalidStateMsg {
+				t.Fatalf("Mismatched error: got:  %q\nwant: %q", err, invalidStateMsg)
+			}
+		})
+	}
+
+	// 2.1. Setting the State to Inactive should succeed.
+	hu, err := hkh.Update(ctx, HMACKeyAttrsToUpdate{
+		State: Inactive,
+	})
+	if err != nil {
+		t.Fatalf("Unexpected Update failure: %v", err)
+	}
+	if got, want := hu.State, Inactive; got != want {
+		t.Fatalf("Unexpected updated state %q, expected %q", got, want)
+	}
+
+	// 2.2. Setting the State back to Active should succeed.
+	hu, err = hkh.Update(ctx, HMACKeyAttrsToUpdate{
+		State: Active,
+	})
+	if err != nil {
+		t.Fatalf("Unexpected Update failure: %v", err)
+	}
+	if got, want := hu.State, Active; got != want {
+		t.Fatalf("Unexpected updated state %q, expected %q", got, want)
+	}
+
+	// 3. Finally set it to back to Inactive and
+	// then retry the deletion which should now succeed.
+	_, _ = hkh.Update(ctx, HMACKeyAttrsToUpdate{
+		State: Inactive,
+	})
+	if err := hkh.Delete(ctx); err != nil {
+		t.Fatalf("Unexpected deletion failure: %v", err)
+	}
+
+	hk, err := hkh.Get(ctx)
+	switch {
+	case err == nil:
+		// If the err == nil, then the returned HMACKey's state MUST be Deleted.
+		if hk == nil || hk.State != Deleted {
+			t.Fatalf("After deletion\nGot %#v\nWanted state %q", hk, Deleted)
+		}
+
+	case err.Error() != "foo":
+		t.Fatalf("Unexpected error: %v", err)
+	}
+}
+
 type testHelper struct {
 	t *testing.T
 }