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