github.com/thiagoyeds/go-cloud@v0.26.0/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  // For V1, see gocloud.dev/aws/ConfigFromURLParams for supported query parameters
   128  // for overriding the aws.Session from the URL.
   129  // For V2, see gocloud.dev/aws/V2ConfigFromURLParams.
   130  type URLOpener struct {
   131  	// UseV2 indicates whether the AWS SDK V2 should be used.
   132  	UseV2 bool
   133  
   134  	// ConfigProvider must be set to a non-nil value if UseV2 is false.
   135  	ConfigProvider client.ConfigProvider
   136  
   137  	// Options specifies the options to pass to OpenKeeper.
   138  	Options KeeperOptions
   139  }
   140  
   141  // OpenKeeperURL opens an AWS KMS Keeper based on u.
   142  func (o *URLOpener) OpenKeeperURL(ctx context.Context, u *url.URL) (*secrets.Keeper, error) {
   143  	// A leading "/" means the Host was empty; trim the slash.
   144  	// This is so that awskms:///foo:bar results in "foo:bar" instead of
   145  	// "/foo:bar".
   146  	keyID := strings.TrimPrefix(path.Join(u.Host, u.Path), "/")
   147  
   148  	if o.UseV2 {
   149  		cfg, err := gcaws.V2ConfigFromURLParams(ctx, u.Query())
   150  		if err != nil {
   151  			return nil, fmt.Errorf("open keeper %v: %v", u, err)
   152  		}
   153  		clientV2, err := DialV2(cfg)
   154  		if err != nil {
   155  			return nil, err
   156  		}
   157  		return OpenKeeperV2(clientV2, keyID, &o.Options), nil
   158  	}
   159  	configProvider := &gcaws.ConfigOverrider{
   160  		Base: o.ConfigProvider,
   161  	}
   162  	overrideCfg, err := gcaws.ConfigFromURLParams(u.Query())
   163  	if err != nil {
   164  		return nil, fmt.Errorf("open keeper %v: %v", u, err)
   165  	}
   166  	configProvider.Configs = append(configProvider.Configs, overrideCfg)
   167  	client, err := Dial(configProvider)
   168  	if err != nil {
   169  		return nil, err
   170  	}
   171  	return OpenKeeper(client, keyID, &o.Options), nil
   172  }
   173  
   174  // OpenKeeper returns a *secrets.Keeper that uses AWS KMS.
   175  // The key ID can be in the form of an Amazon Resource Name (ARN), alias
   176  // name, or alias ARN. See
   177  // https://docs.aws.amazon.com/kms/latest/developerguide/viewing-keys.html#find-cmk-id-arn
   178  // for more details.
   179  // See the package documentation for an example.
   180  func OpenKeeper(client *kms.KMS, keyID string, opts *KeeperOptions) *secrets.Keeper {
   181  	return secrets.NewKeeper(&keeper{
   182  		useV2:  false,
   183  		keyID:  keyID,
   184  		client: client,
   185  	})
   186  }
   187  
   188  // OpenKeeperV2 returns a *secrets.Keeper that uses AWS KMS, using SDK v2.
   189  // The key ID can be in the form of an Amazon Resource Name (ARN), alias
   190  // name, or alias ARN. See
   191  // https://docs.aws.amazon.com/kms/latest/developerguide/viewing-keys.html#find-cmk-id-arn
   192  // for more details.
   193  // See the package documentation for an example.
   194  func OpenKeeperV2(client *kmsv2.Client, keyID string, opts *KeeperOptions) *secrets.Keeper {
   195  	return secrets.NewKeeper(&keeper{
   196  		useV2:    true,
   197  		keyID:    keyID,
   198  		clientV2: client,
   199  	})
   200  }
   201  
   202  type keeper struct {
   203  	useV2    bool
   204  	keyID    string
   205  	client   *kms.KMS
   206  	clientV2 *kmsv2.Client
   207  }
   208  
   209  // Decrypt decrypts the ciphertext into a plaintext.
   210  func (k *keeper) Decrypt(ctx context.Context, ciphertext []byte) ([]byte, error) {
   211  	if k.useV2 {
   212  		result, err := k.clientV2.Decrypt(ctx, &kmsv2.DecryptInput{
   213  			CiphertextBlob: ciphertext,
   214  		})
   215  		if err != nil {
   216  			return nil, err
   217  		}
   218  		return result.Plaintext, nil
   219  	}
   220  	result, err := k.client.Decrypt(&kms.DecryptInput{
   221  		CiphertextBlob: ciphertext,
   222  	})
   223  	if err != nil {
   224  		return nil, err
   225  	}
   226  	return result.Plaintext, nil
   227  }
   228  
   229  // Encrypt encrypts the plaintext into a ciphertext.
   230  func (k *keeper) Encrypt(ctx context.Context, plaintext []byte) ([]byte, error) {
   231  	if k.useV2 {
   232  		result, err := k.clientV2.Encrypt(ctx, &kmsv2.EncryptInput{
   233  			KeyId:     aws.String(k.keyID),
   234  			Plaintext: plaintext,
   235  		})
   236  		if err != nil {
   237  			return nil, err
   238  		}
   239  		return result.CiphertextBlob, nil
   240  	}
   241  	result, err := k.client.Encrypt(&kms.EncryptInput{
   242  		KeyId:     aws.String(k.keyID),
   243  		Plaintext: plaintext,
   244  	})
   245  	if err != nil {
   246  		return nil, err
   247  	}
   248  	return result.CiphertextBlob, nil
   249  }
   250  
   251  // Close implements driver.Keeper.Close.
   252  func (k *keeper) Close() error { return nil }
   253  
   254  // ErrorAs implements driver.Keeper.ErrorAs.
   255  func (k *keeper) ErrorAs(err error, i interface{}) bool {
   256  	if k.useV2 {
   257  		return errors.As(err, i)
   258  	}
   259  	e, ok := err.(awserr.Error)
   260  	if !ok {
   261  		return false
   262  	}
   263  	p, ok := i.(*awserr.Error)
   264  	if !ok {
   265  		return false
   266  	}
   267  	*p = e
   268  	return true
   269  }
   270  
   271  // ErrorCode implements driver.ErrorCode.
   272  func (k *keeper) ErrorCode(err error) gcerrors.ErrorCode {
   273  	var code string
   274  	if k.useV2 {
   275  		var ae smithy.APIError
   276  		if !errors.As(err, &ae) {
   277  			return gcerr.Unknown
   278  		}
   279  		code = ae.ErrorCode()
   280  	} else {
   281  		ae, ok := err.(awserr.Error)
   282  		if !ok {
   283  			return gcerr.Unknown
   284  		}
   285  		code = ae.Code()
   286  	}
   287  	ec, ok := errorCodeMap[code]
   288  	if !ok {
   289  		return gcerr.Unknown
   290  	}
   291  	return ec
   292  }
   293  
   294  var errorCodeMap = map[string]gcerrors.ErrorCode{
   295  	kms.ErrCodeNotFoundException:          gcerrors.NotFound,
   296  	kms.ErrCodeInvalidCiphertextException: gcerrors.InvalidArgument,
   297  	kms.ErrCodeInvalidKeyUsageException:   gcerrors.InvalidArgument,
   298  	kms.ErrCodeInternalException:          gcerrors.Internal,
   299  	kms.ErrCodeInvalidStateException:      gcerrors.FailedPrecondition,
   300  	kms.ErrCodeDisabledException:          gcerrors.PermissionDenied,
   301  	kms.ErrCodeInvalidGrantTokenException: gcerrors.PermissionDenied,
   302  	kms.ErrCodeKeyUnavailableException:    gcerrors.ResourceExhausted,
   303  	kms.ErrCodeDependencyTimeoutException: gcerrors.DeadlineExceeded,
   304  }
   305  
   306  // KeeperOptions controls Keeper behaviors.
   307  // It is provided for future extensibility.
   308  type KeeperOptions struct{}