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 }