github.com/thiagoyeds/go-cloud@v0.26.0/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 Dial, 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 uses DialUsingCLIAuth to get credentials from the
    25  // "az" command line.
    26  //
    27  // To customize the URL opener, or for more details on the URL format,
    28  // see URLOpener.
    29  // See https://gocloud.dev/concepts/urls/ for background information.
    30  //
    31  // As
    32  //
    33  // azurekeyvault exposes the following type for As:
    34  // - Error: autorest.DetailedError, see https://godoc.org/github.com/Azure/go-autorest/autorest#DetailedError
    35  package azurekeyvault
    36  
    37  import (
    38  	"context"
    39  	"encoding/base64"
    40  	"fmt"
    41  	"net/url"
    42  	"os"
    43  	"path"
    44  	"regexp"
    45  	"strconv"
    46  	"strings"
    47  	"sync"
    48  
    49  	"github.com/Azure/azure-sdk-for-go/services/keyvault/v7.0/keyvault"
    50  	"github.com/Azure/go-autorest/autorest"
    51  	"github.com/Azure/go-autorest/autorest/azure/auth"
    52  	"github.com/google/wire"
    53  	"gocloud.dev/gcerrors"
    54  	"gocloud.dev/internal/gcerr"
    55  	"gocloud.dev/internal/useragent"
    56  	"gocloud.dev/secrets"
    57  )
    58  
    59  var (
    60  	// Map of HTTP Status Code to go-cloud ErrorCode
    61  	errorCodeMap = map[int]gcerrors.ErrorCode{
    62  		200: gcerrors.OK,
    63  		400: gcerrors.InvalidArgument,
    64  		401: gcerrors.PermissionDenied,
    65  		403: gcerrors.PermissionDenied,
    66  		404: gcerrors.NotFound,
    67  		408: gcerrors.DeadlineExceeded,
    68  		429: gcerrors.ResourceExhausted,
    69  		500: gcerrors.Internal,
    70  		501: gcerrors.Unimplemented,
    71  	}
    72  )
    73  
    74  func init() {
    75  	secrets.DefaultURLMux().RegisterKeeper(Scheme, new(defaultDialer))
    76  }
    77  
    78  // Set holds Wire providers for this package.
    79  var Set = wire.NewSet(
    80  	Dial,
    81  	wire.Struct(new(URLOpener), "Client"),
    82  )
    83  
    84  // defaultDialer dials Azure KeyVault from the environment on the first call to OpenKeeperURL.
    85  type defaultDialer struct {
    86  	init   sync.Once
    87  	opener *URLOpener
    88  	err    error
    89  }
    90  
    91  func (o *defaultDialer) OpenKeeperURL(ctx context.Context, u *url.URL) (*secrets.Keeper, error) {
    92  	o.init.Do(func() {
    93  		// Determine the dialer to use. The default one gets
    94  		// credentials from the environment, but an alternative is
    95  		// to get credentials from the az CLI.
    96  		dialer := Dial
    97  		useCLIStr := os.Getenv("AZURE_KEYVAULT_AUTH_VIA_CLI")
    98  		if useCLIStr != "" {
    99  			if b, err := strconv.ParseBool(useCLIStr); err != nil {
   100  				o.err = fmt.Errorf("invalid value %q for environment variable AZURE_KEYVAULT_AUTH_VIA_CLI: %v", useCLIStr, err)
   101  				return
   102  			} else if b {
   103  				dialer = DialUsingCLIAuth
   104  			}
   105  		}
   106  		client, err := dialer()
   107  		if err != nil {
   108  			o.err = err
   109  			return
   110  		}
   111  		o.opener = &URLOpener{Client: client}
   112  	})
   113  	if o.err != nil {
   114  		return nil, fmt.Errorf("open keeper %v: failed to Dial default KeyVault: %v", u, o.err)
   115  	}
   116  	return o.opener.OpenKeeperURL(ctx, u)
   117  }
   118  
   119  // Scheme is the URL scheme azurekeyvault registers its URLOpener under on secrets.DefaultMux.
   120  const Scheme = "azurekeyvault"
   121  
   122  // URLOpener opens Azure KeyVault URLs like
   123  // "azurekeyvault://{keyvault-name}.vault.azure.net/keys/{key-name}/{key-version}?algorithm=RSA-OAEP-256".
   124  //
   125  // The "azurekeyvault" URL scheme is replaced with "https" to construct an Azure
   126  // Key Vault keyID, as described in https://docs.microsoft.com/en-us/azure/key-vault/about-keys-secrets-and-certificates.
   127  // The "/{key-version}"" suffix is optional; it defaults to the latest version.
   128  //
   129  // The "algorithm" query parameter sets the algorithm to use; see
   130  // https://docs.microsoft.com/en-us/rest/api/keyvault/encrypt/encrypt#jsonwebkeyencryptionalgorithm
   131  // for supported algorithms. It defaults to "RSA-OAEP-256".
   132  //
   133  // No other query parameters are supported.
   134  type URLOpener struct {
   135  	// Client must be set to a non-nil value.
   136  	Client *keyvault.BaseClient
   137  
   138  	// Options specifies the options to pass to OpenKeeper.
   139  	Options KeeperOptions
   140  }
   141  
   142  // OpenKeeperURL opens an Azure KeyVault Keeper based on u.
   143  func (o *URLOpener) OpenKeeperURL(ctx context.Context, u *url.URL) (*secrets.Keeper, error) {
   144  	q := u.Query()
   145  	algorithm := q.Get("algorithm")
   146  	if algorithm != "" {
   147  		o.Options.Algorithm = keyvault.JSONWebKeyEncryptionAlgorithm(algorithm)
   148  		q.Del("algorithm")
   149  	}
   150  	for param := range q {
   151  		return nil, fmt.Errorf("open keeper %v: invalid query parameter %q", u, param)
   152  	}
   153  	keyID := "https://" + path.Join(u.Host, u.Path)
   154  	return OpenKeeper(o.Client, keyID, &o.Options)
   155  }
   156  
   157  type keeper struct {
   158  	client      *keyvault.BaseClient
   159  	keyVaultURI string
   160  	keyName     string
   161  	keyVersion  string
   162  	options     *KeeperOptions
   163  }
   164  
   165  // KeeperOptions provides configuration options for encryption/decryption operations.
   166  type KeeperOptions struct {
   167  	// Algorithm sets the encryption algorithm used.
   168  	// Defaults to "RSA-OAEP-256".
   169  	// See https://docs.microsoft.com/en-us/rest/api/keyvault/encrypt/encrypt#jsonwebkeyencryptionalgorithm
   170  	// for more details.
   171  	Algorithm keyvault.JSONWebKeyEncryptionAlgorithm
   172  }
   173  
   174  // Dial gets a new *keyvault.BaseClient using authorization from the environment.
   175  // See https://docs.microsoft.com/en-us/go/azure/azure-sdk-go-authorization#use-environment-based-authentication.
   176  func Dial() (*keyvault.BaseClient, error) {
   177  	return dial(false)
   178  }
   179  
   180  // DialUsingCLIAuth gets a new *keyvault.BaseClient using authorization from the "az" CLI.
   181  func DialUsingCLIAuth() (*keyvault.BaseClient, error) {
   182  	return dial(true)
   183  }
   184  
   185  // dial is a helper for Dial and DialUsingCLIAuth.
   186  func dial(useCLI bool) (*keyvault.BaseClient, error) {
   187  	// Set the resource explicitly, because the default is the "resource manager endpoint"
   188  	// instead of the keyvault endpoint.
   189  	// https://azidentity.azurewebsites.net/post/2018/11/30/azure-key-vault-oauth-resource-value-https-vault-azure-net-no-slash
   190  	// has some discussion.
   191  	resource := os.Getenv("AZURE_AD_RESOURCE")
   192  	if resource == "" {
   193  		resource = "https://vault.azure.net"
   194  	}
   195  	authorizer := auth.NewAuthorizerFromEnvironmentWithResource
   196  	if useCLI {
   197  		authorizer = auth.NewAuthorizerFromCLIWithResource
   198  	}
   199  	auth, err := authorizer(resource)
   200  	if err != nil {
   201  		return nil, err
   202  	}
   203  	client := keyvault.NewWithoutDefaults()
   204  	client.Authorizer = auth
   205  	client.Sender = autorest.NewClientWithUserAgent(useragent.AzureUserAgentPrefix("secrets"))
   206  	return &client, nil
   207  }
   208  
   209  var (
   210  	// Note that the last binding may be just a key, or key/version.
   211  	keyIDRE = regexp.MustCompile(`^(https://.+\.vault\.(?:[a-z\d-.]+)/)keys/(.+)$`)
   212  )
   213  
   214  // OpenKeeper returns a *secrets.Keeper that uses Azure keyVault.
   215  //
   216  // client is a *keyvault.BaseClient instance, see https://godoc.org/github.com/Azure/azure-sdk-for-go/services/keyvault/v7.0/keyvault#BaseClient.
   217  //
   218  // keyID is a Azure Key Vault key identifier like "https://{keyvault-name}.vault.azure.net/keys/{key-name}/{key-version}".
   219  // The "/{key-version}" suffix is optional; it defaults to the latest version.
   220  // See https://docs.microsoft.com/en-us/azure/key-vault/about-keys-secrets-and-certificates
   221  // for more details.
   222  func OpenKeeper(client *keyvault.BaseClient, keyID string, opts *KeeperOptions) (*secrets.Keeper, error) {
   223  	drv, err := openKeeper(client, keyID, opts)
   224  	if err != nil {
   225  		return nil, err
   226  	}
   227  	return secrets.NewKeeper(drv), nil
   228  }
   229  
   230  func openKeeper(client *keyvault.BaseClient, keyID string, opts *KeeperOptions) (*keeper, error) {
   231  	if opts == nil {
   232  		opts = &KeeperOptions{}
   233  	}
   234  	if opts.Algorithm == "" {
   235  		opts.Algorithm = keyvault.RSAOAEP256
   236  	}
   237  	matches := keyIDRE.FindStringSubmatch(keyID)
   238  	if len(matches) != 3 {
   239  		return nil, fmt.Errorf("invalid keyID %q; must match %v %v", keyID, keyIDRE, matches)
   240  	}
   241  	// matches[0] is the whole keyID, [1] is the keyVaultURI, and [2] is the key or the key/version.
   242  	keyVaultURI := matches[1]
   243  	parts := strings.SplitN(matches[2], "/", 2)
   244  	keyName := parts[0]
   245  	var keyVersion string
   246  	if len(parts) > 1 {
   247  		keyVersion = parts[1]
   248  	}
   249  	return &keeper{
   250  		client:      client,
   251  		keyVaultURI: keyVaultURI,
   252  		keyName:     keyName,
   253  		keyVersion:  keyVersion,
   254  		options:     opts,
   255  	}, nil
   256  }
   257  
   258  // Encrypt encrypts the plaintext into a ciphertext.
   259  func (k *keeper) Encrypt(ctx context.Context, plaintext []byte) ([]byte, error) {
   260  	b64Text := base64.StdEncoding.EncodeToString(plaintext)
   261  	keyOpsResult, err := k.client.Encrypt(ctx, k.keyVaultURI, k.keyName, k.keyVersion, keyvault.KeyOperationsParameters{
   262  		Algorithm: k.options.Algorithm,
   263  		Value:     &b64Text,
   264  	})
   265  	if err != nil {
   266  		return nil, err
   267  	}
   268  	return []byte(*keyOpsResult.Result), nil
   269  }
   270  
   271  // Decrypt decrypts the ciphertext into a plaintext.
   272  func (k *keeper) Decrypt(ctx context.Context, ciphertext []byte) ([]byte, error) {
   273  	cipherval := string(ciphertext)
   274  	keyOpsResult, err := k.client.Decrypt(ctx, k.keyVaultURI, k.keyName, k.keyVersion, keyvault.KeyOperationsParameters{
   275  		Algorithm: k.options.Algorithm,
   276  		Value:     &cipherval,
   277  	})
   278  	if err != nil {
   279  		return nil, err
   280  	}
   281  	return base64.RawURLEncoding.DecodeString(*keyOpsResult.Result)
   282  }
   283  
   284  // Close implements driver.Keeper.Close.
   285  func (k *keeper) Close() error { return nil }
   286  
   287  // ErrorAs implements driver.Keeper.ErrorAs.
   288  func (k *keeper) ErrorAs(err error, i interface{}) bool {
   289  	e, ok := err.(autorest.DetailedError)
   290  	if !ok {
   291  		return false
   292  	}
   293  	p, ok := i.(*autorest.DetailedError)
   294  	if !ok {
   295  		return false
   296  	}
   297  	*p = e
   298  	return true
   299  }
   300  
   301  // ErrorCode implements driver.ErrorCode.
   302  func (k *keeper) ErrorCode(err error) gcerrors.ErrorCode {
   303  	de, ok := err.(autorest.DetailedError)
   304  	if !ok {
   305  		return gcerr.Unknown
   306  	}
   307  	ec, ok := errorCodeMap[de.StatusCode.(int)]
   308  	if !ok {
   309  		return gcerr.Unknown
   310  	}
   311  	return ec
   312  }