2019-09-23 21:30:20 +03:00

313 lines
10 KiB
Go

// Copyright 2019 The Go Cloud Development Kit Authors
//
// 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
//
// https://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 azurekeyvault provides a secrets implementation backed by Azure KeyVault.
// See https://docs.microsoft.com/en-us/azure/key-vault/key-vault-whatis for more information.
// Use OpenKeeper to construct a *secrets.Keeper.
//
// URLs
//
// For secrets.OpenKeeper, azurekeyvault registers for the scheme "azurekeyvault".
// The default URL opener will use Dial, which gets default credentials from the
// environment, unless the AZURE_KEYVAULT_AUTH_VIA_CLI environment variable is
// set to true, in which case it uses DialUsingCLIAuth to get credentials from the
// "az" command line.
//
// To customize the URL opener, or for more details on the URL format,
// see URLOpener.
// See https://gocloud.dev/concepts/urls/ for background information.
//
// As
//
// azurekeyvault exposes the following type for As:
// - Error: autorest.DetailedError, see https://godoc.org/github.com/Azure/go-autorest/autorest#DetailedError
package azurekeyvault
import (
"context"
"encoding/base64"
"fmt"
"net/url"
"os"
"path"
"regexp"
"strconv"
"strings"
"sync"
"github.com/Azure/azure-sdk-for-go/services/keyvault/v7.0/keyvault"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/azure/auth"
"github.com/google/wire"
"gocloud.dev/gcerrors"
"gocloud.dev/internal/gcerr"
"gocloud.dev/internal/useragent"
"gocloud.dev/secrets"
)
var (
// Map of HTTP Status Code to go-cloud ErrorCode
errorCodeMap = map[int]gcerrors.ErrorCode{
200: gcerrors.OK,
400: gcerrors.InvalidArgument,
401: gcerrors.PermissionDenied,
403: gcerrors.PermissionDenied,
404: gcerrors.NotFound,
408: gcerrors.DeadlineExceeded,
429: gcerrors.ResourceExhausted,
500: gcerrors.Internal,
501: gcerrors.Unimplemented,
}
)
func init() {
secrets.DefaultURLMux().RegisterKeeper(Scheme, new(defaultDialer))
}
// Set holds Wire providers for this package.
var Set = wire.NewSet(
Dial,
wire.Struct(new(URLOpener), "Client"),
)
// defaultDialer dials Azure KeyVault from the environment on the first call to OpenKeeperURL.
type defaultDialer struct {
init sync.Once
opener *URLOpener
err error
}
func (o *defaultDialer) OpenKeeperURL(ctx context.Context, u *url.URL) (*secrets.Keeper, error) {
o.init.Do(func() {
// Determine the dialer to use. The default one gets
// credentials from the environment, but an alternative is
// to get credentials from the az CLI.
dialer := Dial
useCLIStr := os.Getenv("AZURE_KEYVAULT_AUTH_VIA_CLI")
if useCLIStr != "" {
if b, err := strconv.ParseBool(useCLIStr); err != nil {
o.err = fmt.Errorf("invalid value %q for environment variable AZURE_KEYVAULT_AUTH_VIA_CLI: %v", useCLIStr, err)
return
} else if b {
dialer = DialUsingCLIAuth
}
}
client, err := dialer()
if err != nil {
o.err = err
return
}
o.opener = &URLOpener{Client: client}
})
if o.err != nil {
return nil, fmt.Errorf("open keeper %v: failed to Dial default KeyVault: %v", u, o.err)
}
return o.opener.OpenKeeperURL(ctx, u)
}
// Scheme is the URL scheme azurekeyvault registers its URLOpener under on secrets.DefaultMux.
const Scheme = "azurekeyvault"
// URLOpener opens Azure KeyVault URLs like
// "azurekeyvault://{keyvault-name}.vault.azure.net/keys/{key-name}/{key-version}?algorithm=RSA-OAEP-256".
//
// The "azurekeyvault" URL scheme is replaced with "https" to construct an Azure
// Key Vault keyID, as described in https://docs.microsoft.com/en-us/azure/key-vault/about-keys-secrets-and-certificates.
// The "/{key-version}"" suffix is optional; it defaults to the latest version.
//
// The "algorithm" query parameter sets the algorithm to use; see
// https://docs.microsoft.com/en-us/rest/api/keyvault/encrypt/encrypt#jsonwebkeyencryptionalgorithm
// for supported algorithms. It defaults to "RSA-OAEP-256".
//
// No other query parameters are supported.
type URLOpener struct {
// Client must be set to a non-nil value.
Client *keyvault.BaseClient
// Options specifies the options to pass to OpenKeeper.
Options KeeperOptions
}
// OpenKeeperURL opens an Azure KeyVault Keeper based on u.
func (o *URLOpener) OpenKeeperURL(ctx context.Context, u *url.URL) (*secrets.Keeper, error) {
q := u.Query()
algorithm := q.Get("algorithm")
if algorithm != "" {
o.Options.Algorithm = keyvault.JSONWebKeyEncryptionAlgorithm(algorithm)
q.Del("algorithm")
}
for param := range q {
return nil, fmt.Errorf("open keeper %v: invalid query parameter %q", u, param)
}
keyID := "https://" + path.Join(u.Host, u.Path)
return OpenKeeper(o.Client, keyID, &o.Options)
}
type keeper struct {
client *keyvault.BaseClient
keyVaultURI string
keyName string
keyVersion string
options *KeeperOptions
}
// KeeperOptions provides configuration options for encryption/decryption operations.
type KeeperOptions struct {
// Algorithm sets the encryption algorithm used.
// Defaults to "RSA-OAEP-256".
// See https://docs.microsoft.com/en-us/rest/api/keyvault/encrypt/encrypt#jsonwebkeyencryptionalgorithm
// for more details.
Algorithm keyvault.JSONWebKeyEncryptionAlgorithm
}
// Dial gets a new *keyvault.BaseClient using authorization from the environment.
// See https://docs.microsoft.com/en-us/go/azure/azure-sdk-go-authorization#use-environment-based-authentication.
func Dial() (*keyvault.BaseClient, error) {
return dial(false)
}
// DialUsingCLIAuth gets a new *keyvault.BaseClient using authorization from the "az" CLI.
func DialUsingCLIAuth() (*keyvault.BaseClient, error) {
return dial(true)
}
// dial is a helper for Dial and DialUsingCLIAuth.
func dial(useCLI bool) (*keyvault.BaseClient, error) {
// Set the resource explicitly, because the default is the "resource manager endpoint"
// instead of the keyvault endpoint.
// https://azidentity.azurewebsites.net/post/2018/11/30/azure-key-vault-oauth-resource-value-https-vault-azure-net-no-slash
// has some discussion.
resource := os.Getenv("AZURE_AD_RESOURCE")
if resource == "" {
resource = "https://vault.azure.net"
}
authorizer := auth.NewAuthorizerFromEnvironmentWithResource
if useCLI {
authorizer = auth.NewAuthorizerFromCLIWithResource
}
auth, err := authorizer(resource)
if err != nil {
return nil, err
}
client := keyvault.NewWithoutDefaults()
client.Authorizer = auth
client.Sender = autorest.NewClientWithUserAgent(useragent.AzureUserAgentPrefix("secrets"))
return &client, nil
}
var (
// Note that the last binding may be just a key, or key/version.
keyIDRE = regexp.MustCompile("^(https://.+\\.vault\\.azure\\.net/)keys/(.+)$")
)
// OpenKeeper returns a *secrets.Keeper that uses Azure keyVault.
//
// client is a *keyvault.BaseClient instance, see https://godoc.org/github.com/Azure/azure-sdk-for-go/services/keyvault/v7.0/keyvault#BaseClient.
//
// keyID is a Azure Key Vault key identifier like "https://{keyvault-name}.vault.azure.net/keys/{key-name}/{key-version}".
// The "/{key-version}" suffix is optional; it defaults to the latest version.
// See https://docs.microsoft.com/en-us/azure/key-vault/about-keys-secrets-and-certificates
// for more details.
func OpenKeeper(client *keyvault.BaseClient, keyID string, opts *KeeperOptions) (*secrets.Keeper, error) {
drv, err := openKeeper(client, keyID, opts)
if err != nil {
return nil, err
}
return secrets.NewKeeper(drv), nil
}
func openKeeper(client *keyvault.BaseClient, keyID string, opts *KeeperOptions) (*keeper, error) {
if opts == nil {
opts = &KeeperOptions{}
}
if opts.Algorithm == "" {
opts.Algorithm = keyvault.RSAOAEP256
}
matches := keyIDRE.FindStringSubmatch(keyID)
if len(matches) != 3 {
return nil, fmt.Errorf("invalid keyID %q; must match %v %v", keyID, keyIDRE, matches)
}
// matches[0] is the whole keyID, [1] is the keyVaultURI, and [2] is the key or the key/version.
keyVaultURI := matches[1]
parts := strings.SplitN(matches[2], "/", 2)
keyName := parts[0]
var keyVersion string
if len(parts) > 1 {
keyVersion = parts[1]
}
return &keeper{
client: client,
keyVaultURI: keyVaultURI,
keyName: keyName,
keyVersion: keyVersion,
options: opts,
}, nil
}
// Encrypt encrypts the plaintext into a ciphertext.
func (k *keeper) Encrypt(ctx context.Context, plaintext []byte) ([]byte, error) {
b64Text := base64.StdEncoding.EncodeToString(plaintext)
keyOpsResult, err := k.client.Encrypt(ctx, k.keyVaultURI, k.keyName, k.keyVersion, keyvault.KeyOperationsParameters{
Algorithm: k.options.Algorithm,
Value: &b64Text,
})
if err != nil {
return nil, err
}
return []byte(*keyOpsResult.Result), nil
}
// Decrypt decrypts the ciphertext into a plaintext.
func (k *keeper) Decrypt(ctx context.Context, ciphertext []byte) ([]byte, error) {
cipherval := string(ciphertext)
keyOpsResult, err := k.client.Decrypt(ctx, k.keyVaultURI, k.keyName, k.keyVersion, keyvault.KeyOperationsParameters{
Algorithm: k.options.Algorithm,
Value: &cipherval,
})
if err != nil {
return nil, err
}
return base64.StdEncoding.DecodeString(*keyOpsResult.Result)
}
// Close implements driver.Keeper.Close.
func (k *keeper) Close() error { return nil }
// ErrorAs implements driver.Keeper.ErrorAs.
func (k *keeper) ErrorAs(err error, i interface{}) bool {
e, ok := err.(autorest.DetailedError)
if !ok {
return false
}
p, ok := i.(*autorest.DetailedError)
if !ok {
return false
}
*p = e
return true
}
// ErrorCode implements driver.ErrorCode.
func (k *keeper) ErrorCode(err error) gcerrors.ErrorCode {
de, ok := err.(autorest.DetailedError)
if !ok {
return gcerr.Unknown
}
ec, ok := errorCodeMap[de.StatusCode.(int)]
if !ok {
return gcerr.Unknown
}
return ec
}