aws-sdk-s3 1.73.0 → 1.78.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/lib/aws-sdk-s3.rb +1 -1
  3. data/lib/aws-sdk-s3/bucket.rb +2 -2
  4. data/lib/aws-sdk-s3/client.rb +127 -114
  5. data/lib/aws-sdk-s3/customizations/object.rb +12 -1
  6. data/lib/aws-sdk-s3/encryption.rb +2 -0
  7. data/lib/aws-sdk-s3/encryption/client.rb +11 -0
  8. data/lib/aws-sdk-s3/encryption/decrypt_handler.rb +64 -29
  9. data/lib/aws-sdk-s3/encryption/default_cipher_provider.rb +41 -5
  10. data/lib/aws-sdk-s3/encryption/encrypt_handler.rb +5 -5
  11. data/lib/aws-sdk-s3/encryption/io_decrypter.rb +7 -6
  12. data/lib/aws-sdk-s3/encryption/kms_cipher_provider.rb +32 -3
  13. data/lib/aws-sdk-s3/encryption/utils.rb +23 -0
  14. data/lib/aws-sdk-s3/encryptionV2/client.rb +201 -23
  15. data/lib/aws-sdk-s3/encryptionV2/decrypt_handler.rb +40 -12
  16. data/lib/aws-sdk-s3/encryptionV2/default_cipher_provider.rb +77 -10
  17. data/lib/aws-sdk-s3/encryptionV2/default_key_provider.rb +2 -0
  18. data/lib/aws-sdk-s3/encryptionV2/encrypt_handler.rb +7 -4
  19. data/lib/aws-sdk-s3/encryptionV2/errors.rb +24 -0
  20. data/lib/aws-sdk-s3/encryptionV2/io_auth_decrypter.rb +2 -0
  21. data/lib/aws-sdk-s3/encryptionV2/io_decrypter.rb +2 -0
  22. data/lib/aws-sdk-s3/encryptionV2/io_encrypter.rb +2 -0
  23. data/lib/aws-sdk-s3/encryptionV2/key_provider.rb +2 -0
  24. data/lib/aws-sdk-s3/encryptionV2/kms_cipher_provider.rb +90 -20
  25. data/lib/aws-sdk-s3/encryptionV2/materials.rb +2 -0
  26. data/lib/aws-sdk-s3/encryptionV2/utils.rb +2 -15
  27. data/lib/aws-sdk-s3/encryption_v2.rb +4 -1
  28. data/lib/aws-sdk-s3/file_uploader.rb +11 -0
  29. data/lib/aws-sdk-s3/multipart_file_uploader.rb +37 -2
  30. data/lib/aws-sdk-s3/multipart_upload_part.rb +1 -1
  31. data/lib/aws-sdk-s3/object.rb +1 -1
  32. data/lib/aws-sdk-s3/object_summary.rb +19 -3
  33. data/lib/aws-sdk-s3/plugins/accelerate.rb +27 -38
  34. data/lib/aws-sdk-s3/plugins/dualstack.rb +3 -1
  35. data/lib/aws-sdk-s3/plugins/sse_cpk.rb +1 -1
  36. data/lib/aws-sdk-s3/presigned_post.rb +61 -28
  37. data/lib/aws-sdk-s3/presigner.rb +2 -2
  38. data/lib/aws-sdk-s3/resource.rb +1 -1
  39. data/lib/aws-sdk-s3/types.rb +25 -8
  40. metadata +4 -4
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'base64'
2
4
 
3
5
  module Aws
@@ -8,27 +10,36 @@ module Aws
8
10
 
9
11
  def initialize(options = {})
10
12
  @key_provider = options[:key_provider]
13
+ @key_wrap_schema = validate_key_wrap(
14
+ options[:key_wrap_schema],
15
+ @key_provider.encryption_materials.key
16
+ )
17
+ @content_encryption_schema = validate_cek(
18
+ options[:content_encryption_schema]
19
+ )
11
20
  end
12
21
 
13
22
  # @return [Array<Hash,Cipher>] Creates an returns a new encryption
14
23
  # envelope and encryption cipher.
15
24
  def encryption_cipher(options = {})
25
+ validate_options(options)
16
26
  cipher = Utils.aes_encryption_cipher(:GCM)
17
- cek_alg = 'AES/GCM/NoPadding'
18
27
  if @key_provider.encryption_materials.key.is_a? OpenSSL::PKey::RSA
19
- wrap_alg = 'RSA-OAEP-SHA1'
20
- enc_key = encode64(encrypt_rsa(envelope_key(cipher), cek_alg))
28
+ enc_key = encode64(
29
+ encrypt_rsa(envelope_key(cipher), @content_encryption_schema)
30
+ )
21
31
  else
22
- wrap_alg = 'AES/GCM'
23
- enc_key = encode64(encrypt_aes_gcm(envelope_key(cipher), cek_alg))
32
+ enc_key = encode64(
33
+ encrypt_aes_gcm(envelope_key(cipher), @content_encryption_schema)
34
+ )
24
35
  end
25
36
  envelope = {
26
37
  'x-amz-key-v2' => enc_key,
27
- 'x-amz-cek-alg' => cek_alg,
28
- 'x-amz-tag-len' => 16 * 8,
29
- 'x-amz-wrap-alg' => wrap_alg,
38
+ 'x-amz-cek-alg' => @content_encryption_schema,
39
+ 'x-amz-tag-len' => (AES_GCM_TAG_LEN_BYTES * 8).to_s,
40
+ 'x-amz-wrap-alg' => @key_wrap_schema,
30
41
  'x-amz-iv' => encode64(envelope_iv(cipher)),
31
- 'x-amz-matdesc' => materials_description,
42
+ 'x-amz-matdesc' => materials_description
32
43
  }
33
44
  cipher.auth_data = '' # auth_data must be set after key and iv
34
45
  [envelope, cipher]
@@ -37,8 +48,12 @@ module Aws
37
48
  # @return [Cipher] Given an encryption envelope, returns a
38
49
  # decryption cipher.
39
50
  def decryption_cipher(envelope, options = {})
51
+ validate_options(options)
40
52
  master_key = @key_provider.key_for(envelope['x-amz-matdesc'])
41
53
  if envelope.key? 'x-amz-key'
54
+ unless options[:security_profile] == :v2_and_legacy
55
+ raise Errors::LegacyDecryptionError
56
+ end
42
57
  # Support for decryption of legacy objects
43
58
  key = Utils.decrypt(master_key, decode64(envelope['x-amz-key']))
44
59
  iv = decode64(envelope['x-amz-iv'])
@@ -51,13 +66,26 @@ module Aws
51
66
  key =
52
67
  case envelope['x-amz-wrap-alg']
53
68
  when 'AES/GCM'
69
+ if master_key.is_a? OpenSSL::PKey::RSA
70
+ raise ArgumentError, 'Key mismatch - Client is configured' \
71
+ ' with an RSA key and the x-amz-wrap-alg is AES/GCM.'
72
+ end
54
73
  Utils.decrypt_aes_gcm(master_key,
55
74
  decode64(envelope['x-amz-key-v2']),
56
75
  envelope['x-amz-cek-alg'])
57
76
  when 'RSA-OAEP-SHA1'
77
+ unless master_key.is_a? OpenSSL::PKey::RSA
78
+ raise ArgumentError, 'Key mismatch - Client is configured' \
79
+ ' with an AES key and the x-amz-wrap-alg is RSA-OAEP-SHA1.'
80
+ end
58
81
  key, cek_alg = Utils.decrypt_rsa(master_key, decode64(envelope['x-amz-key-v2']))
59
- raise Errors::DecryptionError unless cek_alg == envelope['x-amz-cek-alg']
82
+ raise Errors::CEKAlgMismatchError unless cek_alg == envelope['x-amz-cek-alg']
60
83
  key
84
+ when 'kms+context'
85
+ raise ArgumentError, 'Key mismatch - Client is configured' \
86
+ ' with a user provided key and the x-amz-wrap-alg is' \
87
+ ' kms+context. Please configure the client with the' \
88
+ ' required kms_key_id'
61
89
  else
62
90
  raise ArgumentError, 'Unsupported wrap-alg: ' \
63
91
  "#{envelope['x-amz-wrap-alg']}"
@@ -69,6 +97,39 @@ module Aws
69
97
 
70
98
  private
71
99
 
100
+ # Validate that the key_wrap_schema
101
+ # is valid, supported and matches the provided key.
102
+ # Returns the string version for the x-amz-key-wrap-alg
103
+ def validate_key_wrap(key_wrap_schema, key)
104
+ if key.is_a? OpenSSL::PKey::RSA
105
+ unless key_wrap_schema == :rsa_oaep_sha1
106
+ raise ArgumentError, ':key_wrap_schema must be set to :rsa_oaep_sha1 for RSA keys.'
107
+ end
108
+ else
109
+ unless key_wrap_schema == :aes_gcm
110
+ raise ArgumentError, ':key_wrap_schema must be set to :aes_gcm for AES keys.'
111
+ end
112
+ end
113
+
114
+ case key_wrap_schema
115
+ when :rsa_oaep_sha1 then 'RSA-OAEP-SHA1'
116
+ when :aes_gcm then 'AES/GCM'
117
+ when :kms_context
118
+ raise ArgumentError, 'A kms_key_id is required when using :kms_context.'
119
+ else
120
+ raise ArgumentError, "Unsupported key_wrap_schema: #{key_wrap_schema}"
121
+ end
122
+ end
123
+
124
+ def validate_cek(content_encryption_schema)
125
+ case content_encryption_schema
126
+ when :aes_gcm_no_padding
127
+ "AES/GCM/NoPadding"
128
+ else
129
+ raise ArgumentError, "Unsupported content_encryption_schema: #{content_encryption_schema}"
130
+ end
131
+ end
132
+
72
133
  def envelope_key(cipher)
73
134
  cipher.key = cipher.random_key
74
135
  end
@@ -97,6 +158,12 @@ module Aws
97
158
  Base64.decode64(str)
98
159
  end
99
160
 
161
+ def validate_options(options)
162
+ if !options[:kms_encryption_context].nil?
163
+ raise ArgumentError, 'Cannot provide :kms_encryption_context ' \
164
+ 'with non KMS client.'
165
+ end
166
+ end
100
167
  end
101
168
  end
102
169
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Aws
2
4
  module S3
3
5
  module EncryptionV2
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'base64'
2
4
 
3
5
  module Aws
@@ -45,7 +47,8 @@ module Aws
45
47
  context.params[:metadata] ||= {}
46
48
  context.params[:metadata]['x-amz-unencrypted-content-length'] = io.size
47
49
  if context.params.delete(:content_md5)
48
- raise ArgumentError, 'content_md5 is not supported'
50
+ raise ArgumentError, 'Setting content_md5 on client side '\
51
+ 'encrypted objects is deprecated.'
49
52
  end
50
53
  context.http_response.on_headers do
51
54
  context.params[:body].close
@@ -54,9 +57,9 @@ module Aws
54
57
 
55
58
  def apply_cse_user_agent(context)
56
59
  if context.config.user_agent_suffix.nil?
57
- context.config.user_agent_suffix = 'CSE_V2'
58
- elsif !context.config.user_agent_suffix.include? 'CSE_V2'
59
- context.config.user_agent_suffix += ' CSE_V2'
60
+ context.config.user_agent_suffix = EC_USER_AGENT
61
+ elsif !context.config.user_agent_suffix.include? EC_USER_AGENT
62
+ context.config.user_agent_suffix += " #{EC_USER_AGENT}"
60
63
  end
61
64
  end
62
65
 
@@ -1,12 +1,36 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Aws
2
4
  module S3
3
5
  module EncryptionV2
4
6
  module Errors
5
7
 
8
+ # Generic DecryptionError
6
9
  class DecryptionError < RuntimeError; end
7
10
 
8
11
  class EncryptionError < RuntimeError; end
9
12
 
13
+ # Raised when attempting to decrypt a legacy (V1) encrypted object
14
+ # when using a security_profile that does not support it.
15
+ class LegacyDecryptionError < DecryptionError
16
+ def initialize(*args)
17
+ msg = 'The requested object is ' \
18
+ 'encrypted with V1 encryption schemas that have been disabled ' \
19
+ 'by client configuration security_profile = :v2. Retry with ' \
20
+ ':v2_and_legacy or re-encrypt the object.'
21
+ super(msg)
22
+ end
23
+ end
24
+
25
+ class CEKAlgMismatchError < DecryptionError
26
+ def initialize(*args)
27
+ msg = 'The content encryption algorithm used at encryption time ' \
28
+ 'does not match the algorithm stored for decryption time. ' \
29
+ 'The object may be altered or corrupted.'
30
+ super(msg)
31
+ end
32
+ end
33
+
10
34
  end
11
35
  end
12
36
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Aws
2
4
  module S3
3
5
  module EncryptionV2
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Aws
2
4
  module S3
3
5
  module EncryptionV2
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'stringio'
2
4
  require 'tempfile'
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Aws
2
4
  module S3
3
5
  module EncryptionV2
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'base64'
2
4
 
3
5
  module Aws
@@ -7,15 +9,21 @@ module Aws
7
9
  class KmsCipherProvider
8
10
 
9
11
  def initialize(options = {})
10
- @kms_key_id = options[:kms_key_id]
12
+ @kms_key_id = validate_kms_key(options[:kms_key_id])
11
13
  @kms_client = options[:kms_client]
14
+ @key_wrap_schema = validate_key_wrap(
15
+ options[:key_wrap_schema]
16
+ )
17
+ @content_encryption_schema = validate_cek(
18
+ options[:content_encryption_schema]
19
+ )
12
20
  end
13
21
 
14
22
  # @return [Array<Hash,Cipher>] Creates and returns a new encryption
15
23
  # envelope and encryption cipher.
16
24
  def encryption_cipher(options = {})
17
- cek_alg = 'AES/GCM/NoPadding'
18
- encryption_context = build_encryption_context(cek_alg, options)
25
+ validate_key_for_encryption
26
+ encryption_context = build_encryption_context(@content_encryption_schema, options)
19
27
  key_data = @kms_client.generate_data_key(
20
28
  key_id: @kms_key_id,
21
29
  encryption_context: encryption_context,
@@ -26,9 +34,9 @@ module Aws
26
34
  envelope = {
27
35
  'x-amz-key-v2' => encode64(key_data.ciphertext_blob),
28
36
  'x-amz-iv' => encode64(cipher.iv = cipher.random_iv),
29
- 'x-amz-cek-alg' => cek_alg,
30
- 'x-amz-tag-len' => 16 * 8,
31
- 'x-amz-wrap-alg' => 'kms+context',
37
+ 'x-amz-cek-alg' => @content_encryption_schema,
38
+ 'x-amz-tag-len' => (AES_GCM_TAG_LEN_BYTES * 8).to_s,
39
+ 'x-amz-wrap-alg' => @key_wrap_schema,
32
40
  'x-amz-matdesc' => Json.dump(encryption_context)
33
41
  }
34
42
  cipher.auth_data = '' # auth_data must be set after key and iv
@@ -37,27 +45,45 @@ module Aws
37
45
 
38
46
  # @return [Cipher] Given an encryption envelope, returns a
39
47
  # decryption cipher.
40
- def decryption_cipher(envelope, options={})
48
+ def decryption_cipher(envelope, options = {})
41
49
  encryption_context = Json.load(envelope['x-amz-matdesc'])
42
- cek_alg = envelope['x-amz-wrap-alg'] == 'kms+context' ?
43
- encryption_context['aws:x-amz-cek-alg'] : envelope['x-amz-cek-alg']
44
- if cek_alg != envelope['x-amz-cek-alg']
45
- raise Errors::DecryptionError, 'Value of cek-alg from envelope'\
46
- ' does not match the value in the encryption context'
47
- end
50
+ cek_alg = envelope['x-amz-cek-alg']
51
+
52
+ case envelope['x-amz-wrap-alg']
53
+ when 'kms'
54
+ unless options[:security_profile] == :v2_and_legacy
55
+ raise Errors::LegacyDecryptionError
56
+ end
57
+ when 'kms+context'
58
+ if cek_alg != encryption_context['aws:x-amz-cek-alg']
59
+ raise Errors::CEKAlgMismatchError
60
+ end
48
61
 
49
- if envelope['x-amz-wrap-alg'] == 'kms+context' &&
50
- encryption_context != build_encryption_context(cek_alg, options)
51
- raise Errors::DecryptionError, 'Value of encryption context from'\
52
- ' envelope does not match the provided encryption context'
62
+ if encryption_context != build_encryption_context(cek_alg, options)
63
+ raise Errors::DecryptionError, 'Value of encryption context from'\
64
+ ' envelope does not match the provided encryption context'
65
+ end
66
+ when 'AES/GCM'
67
+ raise ArgumentError, 'Key mismatch - Client is configured' \
68
+ ' with a KMS key and the x-amz-wrap-alg is AES/GCM.'
69
+ when 'RSA-OAEP-SHA1'
70
+ raise ArgumentError, 'Key mismatch - Client is configured' \
71
+ ' with a KMS key and the x-amz-wrap-alg is RSA-OAEP-SHA1.'
72
+ else
73
+ raise ArgumentError, 'Unsupported wrap-alg: ' \
74
+ "#{envelope['x-amz-wrap-alg']}"
53
75
  end
54
76
 
55
- key = @kms_client.decrypt(
77
+ any_cmk_mode = false || options[:kms_allow_decrypt_with_any_cmk]
78
+ decrypt_options = {
56
79
  ciphertext_blob: decode64(envelope['x-amz-key-v2']),
57
80
  encryption_context: encryption_context
58
- ).plaintext
59
-
81
+ }
82
+ unless any_cmk_mode
83
+ decrypt_options[:key_id] = @kms_key_id
84
+ end
60
85
 
86
+ key = @kms_client.decrypt(decrypt_options).plaintext
61
87
  iv = decode64(envelope['x-amz-iv'])
62
88
  block_mode =
63
89
  case cek_alg
@@ -77,9 +103,46 @@ module Aws
77
103
 
78
104
  private
79
105
 
106
+ def validate_key_wrap(key_wrap_schema)
107
+ case key_wrap_schema
108
+ when :kms_context then 'kms+context'
109
+ else
110
+ raise ArgumentError, "Unsupported key_wrap_schema: #{key_wrap_schema}"
111
+ end
112
+ end
113
+
114
+ def validate_cek(content_encryption_schema)
115
+ case content_encryption_schema
116
+ when :aes_gcm_no_padding
117
+ "AES/GCM/NoPadding"
118
+ else
119
+ raise ArgumentError, "Unsupported content_encryption_schema: #{content_encryption_schema}"
120
+ end
121
+ end
122
+
123
+ def validate_kms_key(kms_key_id)
124
+ if kms_key_id.nil? || kms_key_id.length.zero?
125
+ raise ArgumentError, 'KMS CMK ID was not specified. ' \
126
+ 'Please specify a CMK ID, ' \
127
+ 'or set kms_key_id: :kms_allow_decrypt_with_any_cmk to use ' \
128
+ 'any valid CMK from the object.'
129
+ end
130
+
131
+ if kms_key_id.is_a?(Symbol) && kms_key_id != :kms_allow_decrypt_with_any_cmk
132
+ raise ArgumentError, 'kms_key_id must be a valid KMS CMK or be ' \
133
+ 'set to :kms_allow_decrypt_with_any_cmk'
134
+ end
135
+ kms_key_id
136
+ end
137
+
80
138
  def build_encryption_context(cek_alg, options = {})
81
139
  kms_context = (options[:kms_encryption_context] || {})
82
140
  .each_with_object({}) { |(k, v), h| h[k.to_s] = v }
141
+ if kms_context.include? 'aws:x-amz-cek-alg'
142
+ raise ArgumentError, 'Conflict in reserved KMS Encryption Context ' \
143
+ 'key aws:x-amz-cek-alg. This value is reserved for the S3 ' \
144
+ 'Encryption Client and cannot be set by the user.'
145
+ end
83
146
  {
84
147
  'aws:x-amz-cek-alg' => cek_alg
85
148
  }.merge(kms_context)
@@ -93,6 +156,13 @@ module Aws
93
156
  Base64.decode64(str)
94
157
  end
95
158
 
159
+ def validate_key_for_encryption
160
+ if @kms_key_id == :kms_allow_decrypt_with_any_cmk
161
+ raise ArgumentError, 'Unable to encrypt/write objects with '\
162
+ 'kms_key_id = :kms_allow_decrypt_with_any_cmk. Provide ' \
163
+ 'a valid kms_key_id on client construction.'
164
+ end
165
+ end
96
166
  end
97
167
  end
98
168
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'base64'
2
4
 
3
5
  module Aws
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'openssl'
2
4
 
3
5
  module Aws
@@ -6,24 +8,9 @@ module Aws
6
8
  # @api private
7
9
  module Utils
8
10
 
9
- UNSAFE_MSG = "unsafe encryption, data is longer than key length"
10
-
11
11
  class << self
12
12
 
13
- def encrypt(key, data)
14
- case key
15
- when OpenSSL::PKey::RSA # asymmetric encryption
16
- warn(UNSAFE_MSG) if key.public_key.n.num_bits < cipher_size(data)
17
- key.public_encrypt(data)
18
- when String # symmetric encryption
19
- warn(UNSAFE_MSG) if cipher_size(key) < cipher_size(data)
20
- cipher = aes_encryption_cipher(:ECB, key)
21
- cipher.update(data) + cipher.final
22
- end
23
- end
24
-
25
13
  def encrypt_aes_gcm(key, data, auth_data)
26
- warn(UNSAFE_MSG) if cipher_size(key) < cipher_size(data)
27
14
  cipher = aes_encryption_cipher(:GCM, key)
28
15
  cipher.iv = (iv = cipher.random_iv)
29
16
  cipher.auth_data = auth_data
@@ -14,7 +14,10 @@ require 'aws-sdk-s3/encryptionV2/default_key_provider'
14
14
 
15
15
  module Aws
16
16
  module S3
17
- module EncryptionV2; end
17
+ module EncryptionV2
18
+ AES_GCM_TAG_LEN_BYTES = 16
19
+ EC_USER_AGENT = 'S3CryptoV2'
20
+ end
18
21
  end
19
22
  end
20
23