client/cloudkms/cloudkms.go (112 lines of code) (raw):
// Copyright 2021 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 cloudkms contains utilities for communicating with CloudKMS.
package cloudkms
import (
"context"
"fmt"
"hash/crc32"
"cloud.google.com/go/kms/apiv1"
rpb "cloud.google.com/go/kms/apiv1/kmspb"
spb "cloud.google.com/go/kms/apiv1/kmspb"
"github.com/googleapis/gax-go/v2"
"google.golang.org/api/option"
wrapperspb "google.golang.org/protobuf/types/known/wrapperspb"
)
// Client defines an interface compatible with Cloud KMS client.
type Client interface {
GetCryptoKey(context.Context, *spb.GetCryptoKeyRequest, ...gax.CallOption) (*rpb.CryptoKey, error)
Encrypt(context.Context, *spb.EncryptRequest, ...gax.CallOption) (*spb.EncryptResponse, error)
Decrypt(context.Context, *spb.DecryptRequest, ...gax.CallOption) (*spb.DecryptResponse, error)
Close() error
}
func crc32c(data []byte) uint32 {
t := crc32.MakeTable(crc32.Castagnoli)
return crc32.Checksum(data, t)
}
// WrapOpts does xyz.
type WrapOpts struct {
Share []byte
KeyName string
RPCOpts []gax.CallOption
}
// WrapShare uses a KMS client to wrap the given share using Cloud KMS.
func WrapShare(ctx context.Context, client Client, opts WrapOpts) ([]byte, error) {
if client == nil {
return nil, fmt.Errorf("nil client specified")
}
req := &spb.EncryptRequest{
Name: opts.KeyName,
Plaintext: opts.Share,
PlaintextCrc32C: wrapperspb.Int64(int64(crc32c(opts.Share))),
}
result, err := client.Encrypt(ctx, req, opts.RPCOpts...)
if err != nil {
return nil, fmt.Errorf("failed to encrypt: %v", err)
}
if !result.VerifiedPlaintextCrc32C {
return nil, fmt.Errorf("Encrypt: request corrupted in-transit")
}
if int64(crc32c(result.Ciphertext)) != result.CiphertextCrc32C.Value {
return nil, fmt.Errorf("Encrypt: response corrupted in-transit")
}
return result.Ciphertext, nil
}
// UnwrapOpts does xyz.
type UnwrapOpts struct {
Share []byte
KeyName string
}
// UnwrapShare uses a KMS client to unwrap the given share using Cloud KMS.
func UnwrapShare(ctx context.Context, client Client, opts UnwrapOpts) ([]byte, error) {
req := &spb.DecryptRequest{
Name: opts.KeyName,
Ciphertext: opts.Share,
CiphertextCrc32C: wrapperspb.Int64(int64(crc32c(opts.Share))),
}
result, err := client.Decrypt(ctx, req)
if err != nil {
return nil, fmt.Errorf("failed to decrypt ciphertext: %v", err)
}
if int64(crc32c(result.Plaintext)) != result.PlaintextCrc32C.Value {
return nil, fmt.Errorf("Decrypt: response corrupted in-transit")
}
return result.Plaintext, nil
}
// ClientFactory manages singleton instances of KMS Clients mapped to JSON credentials.
type ClientFactory struct {
CredsMap map[string]Client
StetVersion string
newKMSClient func(context.Context, ...option.ClientOption) (*kms.KeyManagementClient, error)
}
// NewClientFactory initializes a ClientMap with the provided version.
func NewClientFactory(version string) *ClientFactory {
return &ClientFactory{
CredsMap: make(map[string]Client),
StetVersion: version,
newKMSClient: kms.NewKeyManagementClient,
}
}
func (m *ClientFactory) createClient(ctx context.Context, credentials string) (Client, error) {
// Set user agent for Cloud KMS API calls.
ua := "STET/"
if m.StetVersion != "" {
ua += m.StetVersion
} else {
ua += "dev"
}
opts := []option.ClientOption{option.WithUserAgent(ua)}
// If credentials were specified, include them in the options.
if len(credentials) != 0 {
opts = append(opts, option.WithCredentialsJSON([]byte(credentials)))
}
return m.newKMSClient(ctx, opts...)
}
// Client returns a KMS Client initialized with the provided credentials. If a client
// with these credentials already exists, it returns that.
func (m *ClientFactory) Client(ctx context.Context, credentials string) (Client, error) {
client, ok := m.CredsMap[credentials]
if !ok {
var err error
client, err = m.createClient(ctx, credentials)
if err != nil {
return nil, fmt.Errorf("error creating new KMS client: %v", err)
}
m.CredsMap[credentials] = client
}
return client, nil
}
// Close iterates through all the clients in the map and closes them.
func (m *ClientFactory) Close() error {
for _, client := range m.CredsMap {
if err := client.Close(); err != nil {
return err
}
}
return nil
}