aws-sdk-s3 1.73.0 → 1.78.0

Sign up to get free protection for your applications and to get access to all the features.
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