github.com/SaurabhDubey-Groww/go-cloud@v0.0.0-20221124105541-b26c29285fd8/secrets/azurekeyvault/akv.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 azurekeyvault provides a secrets implementation backed by Azure KeyVault.
    16  // See https://docs.microsoft.com/en-us/azure/key-vault/key-vault-whatis for more information.
    17  // Use OpenKeeper to construct a *secrets.Keeper.
    18  //
    19  // # URLs
    20  //
    21  // For secrets.OpenKeeper, azurekeyvault registers for the scheme "azurekeyvault".
    22  // The default URL opener will use DefaultClientMaker, which gets default credentials from the
    23  // environment, unless the AZURE_KEYVAULT_AUTH_VIA_CLI environment variable is
    24  // set to true, in which case it gets credentials from the "az" command line.
    25  //
    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  // azurekeyvault exposes the following type for As:
    33  // - Error: *azcore.ResponseError.
    34  package azurekeyvault
    35  
    36  import (
    37  	"context"
    38  	"errors"
    39  	"fmt"
    40  	"net/url"
    41  	"os"
    42  	"path"
    43  	"regexp"
    44  	"strconv"
    45  	"strings"
    46  
    47  	"github.com/Azure/azure-sdk-for-go/sdk/azcore"
    48  	"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
    49  	"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
    50  	"github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys"
    51  	"github.com/google/wire"
    52  	"gocloud.dev/gcerrors"
    53  	"gocloud.dev/internal/gcerr"
    54  	"gocloud.dev/internal/useragent"
    55  	"gocloud.dev/secrets"
    56  )
    57  
    58  var (
    59  	// Map of HTTP Status Code to go-cloud ErrorCode
    60  	errorCodeMap = map[int]gcerrors.ErrorCode{
    61  		200: gcerrors.OK,
    62  		400: gcerrors.InvalidArgument,
    63  		401: gcerrors.PermissionDenied,
    64  		403: gcerrors.PermissionDenied,
    65  		404: gcerrors.NotFound,
    66  		408: gcerrors.DeadlineExceeded,
    67  		429: gcerrors.ResourceExhausted,
    68  		500: gcerrors.Internal,
    69  		501: gcerrors.Unimplemented,
    70  	}
    71  )
    72  
    73  func init() {
    74  	secrets.DefaultURLMux().RegisterKeeper(Scheme, new(defaultDialer))
    75  }
    76  
    77  // Set holds Wire providers for this package.
    78  var Set = wire.NewSet(
    79  	DefaultClientMaker,
    80  	wire.Struct(new(URLOpener), "Client"),
    81  )
    82  
    83  // ClientMakerT is the type of a function used to generate a Client.
    84  type ClientMakerT func(keyVaultURI string) (*azkeys.Client, error)
    85  
    86  // defaultDialer dials Azure KeyVault using DefaultClientMaker.
    87  type defaultDialer struct{}
    88  
    89  func (o *defaultDialer) OpenKeeperURL(ctx context.Context, u *url.URL) (*secrets.Keeper, error) {
    90  	opener := &URLOpener{ClientMaker: DefaultClientMaker}
    91  	return opener.OpenKeeperURL(ctx, u)
    92  }
    93  
    94  // Scheme is the URL scheme azurekeyvault registers its URLOpener under on secrets.DefaultMux.
    95  const Scheme = "azurekeyvault"
    96  
    97  // URLOpener opens Azure KeyVault URLs like
    98  // "azurekeyvault://{keyvault-name}.vault.azure.net/keys/{key-name}/{key-version}?algorithm=RSA-OAEP-256".
    99  //
   100  // The "azurekeyvault" URL scheme is replaced with "https" to construct an Azure
   101  // Key Vault keyID, as described in https://docs.microsoft.com/en-us/azure/key-vault/about-keys-secrets-and-certificates.
   102  // The "/{key-version}"" suffix is optional; it defaults to the latest version.
   103  //
   104  // The "algorithm" query parameter sets the algorithm to use; see
   105  // https://docs.microsoft.com/en-us/rest/api/keyvault/encrypt/encrypt#jsonwebkeyencryptionalgorithm
   106  // for supported algorithms. It defaults to "RSA-OAEP-256".
   107  //
   108  // No other query parameters are supported.
   109  type URLOpener struct {
   110  	// ClientMaker defaults to DefaultClientMaker.
   111  	ClientMaker ClientMakerT
   112  
   113  	// Options specifies the options to pass to OpenKeeper.
   114  	Options KeeperOptions
   115  }
   116  
   117  // OpenKeeperURL opens an Azure KeyVault Keeper based on u.
   118  func (o *URLOpener) OpenKeeperURL(ctx context.Context, u *url.URL) (*secrets.Keeper, error) {
   119  	q := u.Query()
   120  	algorithm := q.Get("algorithm")
   121  	if algorithm != "" {
   122  		o.Options.Algorithm = azkeys.JSONWebKeyEncryptionAlgorithm(algorithm)
   123  		q.Del("algorithm")
   124  	}
   125  	for param := range q {
   126  		return nil, fmt.Errorf("open keeper %v: invalid query parameter %q", u, param)
   127  	}
   128  	keyID := "https://" + path.Join(u.Host, u.Path)
   129  	return OpenKeeper(o.ClientMaker, keyID, &o.Options)
   130  }
   131  
   132  type keeper struct {
   133  	client      *azkeys.Client
   134  	keyVaultURI string // unused, but for validation in tests
   135  	keyName     string
   136  	keyVersion  string
   137  	options     *KeeperOptions
   138  }
   139  
   140  // KeeperOptions provides configuration options for encryption/decryption operations.
   141  type KeeperOptions struct {
   142  	// Algorithm sets the encryption algorithm used.
   143  	// Defaults to "RSA-OAEP-256".
   144  	// See https://docs.microsoft.com/en-us/rest/api/keyvault/encrypt/encrypt#jsonwebkeyencryptionalgorithm
   145  	// for more details.
   146  	Algorithm azkeys.JSONWebKeyEncryptionAlgorithm
   147  
   148  	// EncryptOptions are passed through to Encrypt.
   149  	EncryptOptions *azkeys.EncryptOptions
   150  
   151  	// DecryptOptions are passed through to Decrypt.
   152  	DecryptOptions *azkeys.DecryptOptions
   153  }
   154  
   155  // DefaultClientMaker returns a function that constructs a KeyVault Client.
   156  // By default it uses credentials from the environment;
   157  // See https://docs.microsoft.com/en-us/go/azure/azure-sdk-go-authorization#use-environment-based-authentication.
   158  // If the environment variable AZURE_KEYVAULT_AUTH_VIA_CLI is set to a truthy value, it
   159  // uses credentials from the Azure CLI instead.
   160  func DefaultClientMaker(keyVaultURI string) (*azkeys.Client, error) {
   161  	useCLI := false
   162  	useCLIStr := os.Getenv("AZURE_KEYVAULT_AUTH_VIA_CLI")
   163  	if useCLIStr != "" {
   164  		var err error
   165  		useCLI, err = strconv.ParseBool(useCLIStr)
   166  		if err != nil {
   167  			return nil, fmt.Errorf("invalid value %q for environment variable AZURE_KEYVAULT_AUTH_VIA_CLI: %v", useCLIStr, err)
   168  		}
   169  	}
   170  	var creds azcore.TokenCredential
   171  	var err error
   172  	if useCLI {
   173  		creds, err = azidentity.NewAzureCLICredential(nil)
   174  	} else {
   175  		creds, err = azidentity.NewEnvironmentCredential(nil)
   176  	}
   177  	if err != nil {
   178  		return nil, err
   179  	}
   180  	return azkeys.NewClient(keyVaultURI, creds, &azkeys.ClientOptions{
   181  		ClientOptions: policy.ClientOptions{
   182  			Telemetry: policy.TelemetryOptions{
   183  				ApplicationID: useragent.AzureUserAgentPrefix("secrets"),
   184  			},
   185  		},
   186  	}), nil
   187  }
   188  
   189  var (
   190  	// Note that the last binding may be just a key, or key/version.
   191  	keyIDRE = regexp.MustCompile(`^(https://.+\.vault\.(?:[a-z\d-.]+)/)keys/(.+)$`)
   192  )
   193  
   194  // OpenKeeper returns a *secrets.Keeper that uses Azure keyVault.
   195  //
   196  // clientMaker is used to construct an azkeys.Client.
   197  //
   198  // keyID is a Azure Key Vault key identifier like "https://{keyvault-name}.vault.azure.net/keys/{key-name}/{key-version}".
   199  // The "/{key-version}" suffix is optional; it defaults to the latest version.
   200  // See https://docs.microsoft.com/en-us/azure/key-vault/about-keys-secrets-and-certificates
   201  // for more details.
   202  func OpenKeeper(clientMaker ClientMakerT, keyID string, opts *KeeperOptions) (*secrets.Keeper, error) {
   203  	drv, err := openKeeper(clientMaker, keyID, opts)
   204  	if err != nil {
   205  		return nil, err
   206  	}
   207  	return secrets.NewKeeper(drv), nil
   208  }
   209  
   210  func openKeeper(clientMaker ClientMakerT, keyID string, opts *KeeperOptions) (*keeper, error) {
   211  	if opts == nil {
   212  		opts = &KeeperOptions{}
   213  	}
   214  	if opts.Algorithm == "" {
   215  		opts.Algorithm = azkeys.JSONWebKeyEncryptionAlgorithmRSAOAEP256
   216  	}
   217  	matches := keyIDRE.FindStringSubmatch(keyID)
   218  	if len(matches) != 3 {
   219  		return nil, fmt.Errorf("invalid keyID %q; must match %v %v", keyID, keyIDRE, matches)
   220  	}
   221  	// matches[0] is the whole keyID, [1] is the keyVaultURI, and [2] is the key or the key/version.
   222  	keyVaultURI := matches[1]
   223  	parts := strings.SplitN(matches[2], "/", 2)
   224  	keyName := parts[0]
   225  	var keyVersion string
   226  	if len(parts) > 1 {
   227  		keyVersion = parts[1]
   228  	}
   229  	client, err := clientMaker(keyVaultURI)
   230  	if err != nil {
   231  		return nil, err
   232  	}
   233  	return &keeper{
   234  		client:      client,
   235  		keyVaultURI: keyVaultURI,
   236  		keyName:     keyName,
   237  		keyVersion:  keyVersion,
   238  		options:     opts,
   239  	}, nil
   240  }
   241  
   242  // Encrypt encrypts the plaintext into a ciphertext.
   243  func (k *keeper) Encrypt(ctx context.Context, plaintext []byte) ([]byte, error) {
   244  	keyOpsResult, err := k.client.Encrypt(ctx, k.keyName, k.keyVersion, azkeys.KeyOperationsParameters{
   245  		Algorithm: &k.options.Algorithm,
   246  		Value:     plaintext,
   247  	}, k.options.EncryptOptions)
   248  	if err != nil {
   249  		return nil, err
   250  	}
   251  	return keyOpsResult.Result, nil
   252  }
   253  
   254  // Decrypt decrypts the ciphertext into a plaintext.
   255  func (k *keeper) Decrypt(ctx context.Context, ciphertext []byte) ([]byte, error) {
   256  	keyOpsResult, err := k.client.Decrypt(ctx, k.keyName, k.keyVersion, azkeys.KeyOperationsParameters{
   257  		Algorithm: &k.options.Algorithm,
   258  		Value:     ciphertext,
   259  	}, k.options.DecryptOptions)
   260  	if err != nil {
   261  		return nil, err
   262  	}
   263  	return keyOpsResult.Result, nil
   264  }
   265  
   266  // Close implements driver.Keeper.Close.
   267  func (k *keeper) Close() error { return nil }
   268  
   269  // ErrorAs implements driver.Keeper.ErrorAs.
   270  func (k *keeper) ErrorAs(err error, i interface{}) bool {
   271  	return errors.As(err, i)
   272  }
   273  
   274  // ErrorCode implements driver.ErrorCode.
   275  func (k *keeper) ErrorCode(err error) gcerrors.ErrorCode {
   276  	re, ok := err.(*azcore.ResponseError)
   277  	if !ok {
   278  		return gcerr.Unknown
   279  	}
   280  	ec, ok := errorCodeMap[re.StatusCode]
   281  	if !ok {
   282  		return gcerr.Unknown
   283  	}
   284  	return ec
   285  }