github.com/yorinasub17/go-cloud@v0.27.40/secrets/awskms/kms.go (about)

     1  // Copyright 2019 The Go Cloud Development Kit Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     https://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package awskms provides a secrets implementation backed by AWS KMS.
    16  // Use OpenKeeper to construct a *secrets.Keeper, or OpenKeeperV2 to
    17  // use AWS SDK V2.
    18  //
    19  // # URLs
    20  //
    21  // For secrets.OpenKeeper, awskms registers for the scheme "awskms".
    22  // The default URL opener will use an AWS session with the default credentials
    23  // and configuration; see https://docs.aws.amazon.com/sdk-for-go/api/aws/session/
    24  // for more details.
    25  // Use "awssdk=v1" or "awssdk=v2" to force a specific AWS SDK version.
    26  // To customize the URL opener, or for more details on the URL format,
    27  // see URLOpener.
    28  // See https://gocloud.dev/concepts/urls/ for background information.
    29  //
    30  // # As
    31  //
    32  // awskms exposes the following type for As:
    33  //   - Error: (V1) awserr.Error, (V2) any error type returned by the service, notably smithy.APIError
    34  package awskms // import "gocloud.dev/secrets/awskms"
    35  
    36  import (
    37  	"context"
    38  	"errors"
    39  	"fmt"
    40  	"net/url"
    41  	"path"
    42  	"strings"
    43  	"sync"
    44  
    45  	awsv2 "github.com/aws/aws-sdk-go-v2/aws"
    46  	kmsv2 "github.com/aws/aws-sdk-go-v2/service/kms"
    47  	"github.com/aws/aws-sdk-go/aws"
    48  	"github.com/aws/aws-sdk-go/aws/awserr"
    49  	"github.com/aws/aws-sdk-go/aws/client"
    50  	"github.com/aws/aws-sdk-go/service/kms"
    51  	"github.com/aws/smithy-go"
    52  	"github.com/google/wire"
    53  	gcaws "gocloud.dev/aws"
    54  	"gocloud.dev/gcerrors"
    55  	"gocloud.dev/internal/gcerr"
    56  	"gocloud.dev/secrets"
    57  )
    58  
    59  func init() {
    60  	secrets.DefaultURLMux().RegisterKeeper(Scheme, new(lazySessionOpener))
    61  }
    62  
    63  // Set holds Wire providers for this package.
    64  var Set = wire.NewSet(
    65  	wire.Struct(new(URLOpener), "ConfigProvider"),
    66  	Dial,
    67  	DialV2,
    68  )
    69  
    70  // Dial gets an AWS KMS service client.
    71  func Dial(p client.ConfigProvider) (*kms.KMS, error) {
    72  	if p == nil {
    73  		return nil, errors.New("getting KMS service: no AWS session provided")
    74  	}
    75  	return kms.New(p), nil
    76  }
    77  
    78  // DialV2 gets an AWS KMS service client using the AWS SDK V2.
    79  func DialV2(cfg awsv2.Config) (*kmsv2.Client, error) {
    80  	return kmsv2.NewFromConfig(cfg), nil
    81  }
    82  
    83  // lazySessionOpener obtains the AWS session from the environment on the first
    84  // call to OpenKeeperURL.
    85  type lazySessionOpener struct {
    86  	init   sync.Once
    87  	opener *URLOpener
    88  	err    error
    89  }
    90  
    91  func (o *lazySessionOpener) OpenKeeperURL(ctx context.Context, u *url.URL) (*secrets.Keeper, error) {
    92  	if gcaws.UseV2(u.Query()) {
    93  		opener := &URLOpener{UseV2: true}
    94  		return opener.OpenKeeperURL(ctx, u)
    95  	}
    96  	o.init.Do(func() {
    97  		sess, err := gcaws.NewDefaultSession()
    98  		if err != nil {
    99  			o.err = err
   100  			return
   101  		}
   102  		o.opener = &URLOpener{
   103  			UseV2:          false,
   104  			ConfigProvider: sess,
   105  		}
   106  	})
   107  	if o.err != nil {
   108  		return nil, fmt.Errorf("open keeper %v: %v", u, o.err)
   109  	}
   110  	return o.opener.OpenKeeperURL(ctx, u)
   111  }
   112  
   113  // Scheme is the URL scheme awskms registers its URLOpener under on secrets.DefaultMux.
   114  const Scheme = "awskms"
   115  
   116  // URLOpener opens AWS KMS URLs like "awskms://keyID" or "awskms:///keyID".
   117  //
   118  // The URL Host + Path are used as the key ID, which can be in the form of an
   119  // Amazon Resource Name (ARN), alias name, or alias ARN. See
   120  // https://docs.aws.amazon.com/kms/latest/developerguide/viewing-keys.html#find-cmk-id-arn
   121  // for more details. Note that ARNs may contain ":" characters, which cannot be
   122  // escaped in the Host part of a URL, so the "awskms:///<ARN>" form should be used.
   123  //
   124  // Use "awssdk=v1" to force using AWS SDK v1, "awssdk=v2" to force using AWS SDK v2,
   125  // or anything else to accept the default.
   126  //
   127  // EncryptionContext key/value pairs can be provided by providing URL parameters prefixed
   128  // with "context_"; e.g., "...&context_abc=foo&context_def=bar" would result in
   129  // an EncryptionContext of {abc=foo, def=bar}.
   130  // See https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#encrypt_context.
   131  //
   132  // For V1, see gocloud.dev/aws/ConfigFromURLParams for supported query parameters
   133  // for overriding the aws.Session from the URL.
   134  // For V2, see gocloud.dev/aws/V2ConfigFromURLParams.
   135  type URLOpener struct {
   136  	// UseV2 indicates whether the AWS SDK V2 should be used.
   137  	UseV2 bool
   138  
   139  	// ConfigProvider must be set to a non-nil value if UseV2 is false.
   140  	ConfigProvider client.ConfigProvider
   141  
   142  	// Options specifies the options to pass to OpenKeeper.
   143  	// EncryptionContext parameters from the URL are merged in.
   144  	Options KeeperOptions
   145  }
   146  
   147  // addEncryptionContextFromURLParams merges any EncryptionContext URL parameters from
   148  // u into opts.EncryptionParameters.
   149  // It removes the processed URL parameters from u.
   150  func addEncryptionContextFromURLParams(opts *KeeperOptions, u url.Values) error {
   151  	for k, vs := range u {
   152  		if strings.HasPrefix(k, "context_") {
   153  			if len(vs) != 1 {
   154  				return fmt.Errorf("open keeper: EncryptionContext URL parameters %q must have exactly 1 value", k)
   155  			}
   156  			u.Del(k)
   157  			if opts.EncryptionContext == nil {
   158  				opts.EncryptionContext = map[string]string{}
   159  			}
   160  			opts.EncryptionContext[k[8:]] = vs[0]
   161  		}
   162  	}
   163  	return nil
   164  }
   165  
   166  // OpenKeeperURL opens an AWS KMS Keeper based on u.
   167  func (o *URLOpener) OpenKeeperURL(ctx context.Context, u *url.URL) (*secrets.Keeper, error) {
   168  	// A leading "/" means the Host was empty; trim the slash.
   169  	// This is so that awskms:///foo:bar results in "foo:bar" instead of
   170  	// "/foo:bar".
   171  	keyID := strings.TrimPrefix(path.Join(u.Host, u.Path), "/")
   172  
   173  	queryParams := u.Query()
   174  	opts := o.Options
   175  	if err := addEncryptionContextFromURLParams(&opts, queryParams); err != nil {
   176  		return nil, err
   177  	}
   178  
   179  	if o.UseV2 {
   180  		cfg, err := gcaws.V2ConfigFromURLParams(ctx, queryParams)
   181  		if err != nil {
   182  			return nil, fmt.Errorf("open keeper %v: %v", u, err)
   183  		}
   184  		clientV2, err := DialV2(cfg)
   185  		if err != nil {
   186  			return nil, err
   187  		}
   188  		return OpenKeeperV2(clientV2, keyID, &opts), nil
   189  	}
   190  	configProvider := &gcaws.ConfigOverrider{
   191  		Base: o.ConfigProvider,
   192  	}
   193  	overrideCfg, err := gcaws.ConfigFromURLParams(queryParams)
   194  	if err != nil {
   195  		return nil, fmt.Errorf("open keeper %v: %v", u, err)
   196  	}
   197  	configProvider.Configs = append(configProvider.Configs, overrideCfg)
   198  	client, err := Dial(configProvider)
   199  	if err != nil {
   200  		return nil, err
   201  	}
   202  	return OpenKeeper(client, keyID, &opts), nil
   203  }
   204  
   205  // OpenKeeper returns a *secrets.Keeper that uses AWS KMS.
   206  // The key ID can be in the form of an Amazon Resource Name (ARN), alias
   207  // name, or alias ARN. See
   208  // https://docs.aws.amazon.com/kms/latest/developerguide/viewing-keys.html#find-cmk-id-arn
   209  // for more details.
   210  // See the package documentation for an example.
   211  func OpenKeeper(client *kms.KMS, keyID string, opts *KeeperOptions) *secrets.Keeper {
   212  	if opts == nil {
   213  		opts = &KeeperOptions{}
   214  	}
   215  	return secrets.NewKeeper(&keeper{
   216  		useV2:  false,
   217  		keyID:  keyID,
   218  		client: client,
   219  		opts:   *opts,
   220  	})
   221  }
   222  
   223  // OpenKeeperV2 returns a *secrets.Keeper that uses AWS KMS, using SDK v2.
   224  // The key ID can be in the form of an Amazon Resource Name (ARN), alias
   225  // name, or alias ARN. See
   226  // https://docs.aws.amazon.com/kms/latest/developerguide/viewing-keys.html#find-cmk-id-arn
   227  // for more details.
   228  // See the package documentation for an example.
   229  func OpenKeeperV2(client *kmsv2.Client, keyID string, opts *KeeperOptions) *secrets.Keeper {
   230  	if opts == nil {
   231  		opts = &KeeperOptions{}
   232  	}
   233  	return secrets.NewKeeper(&keeper{
   234  		useV2:    true,
   235  		keyID:    keyID,
   236  		clientV2: client,
   237  		opts:     *opts,
   238  	})
   239  }
   240  
   241  type keeper struct {
   242  	useV2    bool
   243  	keyID    string
   244  	opts     KeeperOptions
   245  	client   *kms.KMS
   246  	clientV2 *kmsv2.Client
   247  }
   248  
   249  func (k *keeper) v1EncryptionContext() map[string]*string {
   250  	if len(k.opts.EncryptionContext) == 0 {
   251  		return nil
   252  	}
   253  	ec := map[string]*string{}
   254  	for k, v := range k.opts.EncryptionContext {
   255  		ec[k] = &v
   256  	}
   257  	return ec
   258  }
   259  
   260  // Decrypt decrypts the ciphertext into a plaintext.
   261  func (k *keeper) Decrypt(ctx context.Context, ciphertext []byte) ([]byte, error) {
   262  	if k.useV2 {
   263  		result, err := k.clientV2.Decrypt(ctx, &kmsv2.DecryptInput{
   264  			CiphertextBlob:    ciphertext,
   265  			EncryptionContext: k.opts.EncryptionContext,
   266  		})
   267  		if err != nil {
   268  			return nil, err
   269  		}
   270  		return result.Plaintext, nil
   271  	}
   272  	result, err := k.client.Decrypt(&kms.DecryptInput{
   273  		CiphertextBlob:    ciphertext,
   274  		EncryptionContext: k.v1EncryptionContext(),
   275  	})
   276  	if err != nil {
   277  		return nil, err
   278  	}
   279  	return result.Plaintext, nil
   280  }
   281  
   282  // Encrypt encrypts the plaintext into a ciphertext.
   283  func (k *keeper) Encrypt(ctx context.Context, plaintext []byte) ([]byte, error) {
   284  	if k.useV2 {
   285  		result, err := k.clientV2.Encrypt(ctx, &kmsv2.EncryptInput{
   286  			KeyId:             aws.String(k.keyID),
   287  			Plaintext:         plaintext,
   288  			EncryptionContext: k.opts.EncryptionContext,
   289  		})
   290  		if err != nil {
   291  			return nil, err
   292  		}
   293  		return result.CiphertextBlob, nil
   294  	}
   295  	result, err := k.client.Encrypt(&kms.EncryptInput{
   296  		KeyId:             aws.String(k.keyID),
   297  		Plaintext:         plaintext,
   298  		EncryptionContext: k.v1EncryptionContext(),
   299  	})
   300  	if err != nil {
   301  		return nil, err
   302  	}
   303  	return result.CiphertextBlob, nil
   304  }
   305  
   306  // Close implements driver.Keeper.Close.
   307  func (k *keeper) Close() error { return nil }
   308  
   309  // ErrorAs implements driver.Keeper.ErrorAs.
   310  func (k *keeper) ErrorAs(err error, i interface{}) bool {
   311  	if k.useV2 {
   312  		return errors.As(err, i)
   313  	}
   314  	e, ok := err.(awserr.Error)
   315  	if !ok {
   316  		return false
   317  	}
   318  	p, ok := i.(*awserr.Error)
   319  	if !ok {
   320  		return false
   321  	}
   322  	*p = e
   323  	return true
   324  }
   325  
   326  // ErrorCode implements driver.ErrorCode.
   327  func (k *keeper) ErrorCode(err error) gcerrors.ErrorCode {
   328  	var code string
   329  	if k.useV2 {
   330  		var ae smithy.APIError
   331  		if !errors.As(err, &ae) {
   332  			return gcerr.Unknown
   333  		}
   334  		code = ae.ErrorCode()
   335  	} else {
   336  		ae, ok := err.(awserr.Error)
   337  		if !ok {
   338  			return gcerr.Unknown
   339  		}
   340  		code = ae.Code()
   341  	}
   342  	ec, ok := errorCodeMap[code]
   343  	if !ok {
   344  		return gcerr.Unknown
   345  	}
   346  	return ec
   347  }
   348  
   349  var errorCodeMap = map[string]gcerrors.ErrorCode{
   350  	kms.ErrCodeNotFoundException:          gcerrors.NotFound,
   351  	kms.ErrCodeInvalidCiphertextException: gcerrors.InvalidArgument,
   352  	kms.ErrCodeInvalidKeyUsageException:   gcerrors.InvalidArgument,
   353  	kms.ErrCodeInternalException:          gcerrors.Internal,
   354  	kms.ErrCodeInvalidStateException:      gcerrors.FailedPrecondition,
   355  	kms.ErrCodeDisabledException:          gcerrors.PermissionDenied,
   356  	kms.ErrCodeInvalidGrantTokenException: gcerrors.PermissionDenied,
   357  	kms.ErrCodeKeyUnavailableException:    gcerrors.ResourceExhausted,
   358  	kms.ErrCodeDependencyTimeoutException: gcerrors.DeadlineExceeded,
   359  }
   360  
   361  // KeeperOptions controls Keeper behaviors.
   362  // It is provided for future extensibility.
   363  type KeeperOptions struct {
   364  	// EncryptionContext parameters.
   365  	// See https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#encrypt_context.
   366  	EncryptionContext map[string]string
   367  }