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