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
@@ -296,6 +296,14 @@ module Aws
296
296
  # etag = response.etag
297
297
  # end
298
298
  #
299
+ # You can provide a callback to monitor progress of the upload:
300
+ #
301
+ # # bytes and totals are each an array with 1 entry per part
302
+ # progress = Proc.new do |bytes, totals|
303
+ # puts bytes.map.with_index { |b, i| "Part #{i+1}: #{b} / #{totals[i]}"}.join(' ') + "Total: #{100.0 * bytes.sum / totals.sum }%" }
304
+ # end
305
+ # obj.upload_file('/path/to/file')
306
+ #
299
307
  # @param [String, Pathname, File, Tempfile] source A file on the local
300
308
  # file system that will be uploaded as this object. This can either be
301
309
  # a String or Pathname to the file, an open File object, or an open
@@ -312,6 +320,10 @@ module Aws
312
320
  # multipart uploads. This option is not used if the file is smaller than
313
321
  # `:multipart_threshold`.
314
322
  #
323
+ # @option options [Proc] :progress_callback
324
+ # A Proc that will be called when each chunk of the upload is sent.
325
+ # It will be invoked with [bytes_read], [total_sizes]
326
+ #
315
327
  # @raise [MultipartUploadError] If an object is being uploaded in
316
328
  # parts, and the upload can not be completed, then the upload is
317
329
  # aborted and this error is raised. The raised error has a `#errors`
@@ -320,7 +332,6 @@ module Aws
320
332
  #
321
333
  # @return [Boolean] Returns `true` when the object is uploaded
322
334
  # without any errors.
323
- #
324
335
  def upload_file(source, options = {})
325
336
  uploading_options = options.dup
326
337
  uploader = FileUploader.new(
@@ -17,5 +17,7 @@ require 'aws-sdk-s3/encryption/default_key_provider'
17
17
  module Aws
18
18
  module S3
19
19
  module Encryption; end
20
+ AES_GCM_TAG_LEN_BYTES = 16
21
+ EC_USER_AGENT = 'S3CryptoV1n'
20
22
  end
21
23
  end
@@ -5,6 +5,10 @@ require 'forwardable'
5
5
  module Aws
6
6
  module S3
7
7
 
8
+ # [MAINTENANCE MODE] There is a new version of the Encryption Client.
9
+ # AWS strongly recommends upgrading to the {Aws::S3::EncryptionV2::Client},
10
+ # which provides updated data security best practices.
11
+ # See documentation for {Aws::S3::EncryptionV2::Client}.
8
12
  # Provides an encryption client that encrypts and decrypts data client-side,
9
13
  # storing the encrypted data in Amazon S3.
10
14
  #
@@ -229,6 +233,13 @@ module Aws
229
233
  @envelope_location = extract_location(options)
230
234
  @instruction_file_suffix = extract_suffix(options)
231
235
  end
236
+ deprecated :initialize,
237
+ message:
238
+ '[MAINTENANCE MODE] This version of the S3 Encryption client is currently in maintenance mode. ' \
239
+ 'AWS strongly recommends upgrading to the Aws::S3::EncryptionV2::Client, ' \
240
+ 'which provides updated data security best practices. ' \
241
+ 'See documentation for Aws::S3::EncryptionV2::Client.'
242
+
232
243
 
233
244
  # @return [S3::Client]
234
245
  attr_reader :client
@@ -7,6 +7,7 @@ module Aws
7
7
  module Encryption
8
8
  # @api private
9
9
  class DecryptHandler < Seahorse::Client::Handler
10
+ @@warned_response_target_proc = false
10
11
 
11
12
  V1_ENVELOPE_KEYS = %w(
12
13
  x-amz-key
@@ -22,7 +23,17 @@ module Aws
22
23
  x-amz-matdesc
23
24
  )
24
25
 
25
- POSSIBLE_ENVELOPE_KEYS = (V1_ENVELOPE_KEYS + V2_ENVELOPE_KEYS).uniq
26
+ V2_OPTIONAL_KEYS = %w(x-amz-tag-len)
27
+
28
+ POSSIBLE_ENVELOPE_KEYS = (V1_ENVELOPE_KEYS +
29
+ V2_ENVELOPE_KEYS + V2_OPTIONAL_KEYS).uniq
30
+
31
+ POSSIBLE_WRAPPING_FORMATS = %w(
32
+ AES/GCM
33
+ kms
34
+ kms+context
35
+ RSA-OAEP-SHA1
36
+ )
26
37
 
27
38
  POSSIBLE_ENCRYPTION_FORMATS = %w(
28
39
  AES/GCM/NoPadding
@@ -30,9 +41,21 @@ module Aws
30
41
  AES/CBC/PKCS7Padding
31
42
  )
32
43
 
44
+ AUTH_REQUIRED_CEK_ALGS = %w(AES/GCM/NoPadding)
45
+
33
46
  def call(context)
34
47
  attach_http_event_listeners(context)
35
48
  apply_cse_user_agent(context)
49
+
50
+ if context[:response_target].is_a?(Proc) && !@@warned_response_target_proc
51
+ @@warned_response_target_proc = true
52
+ warn(':response_target is a Proc, or a block was provided. ' \
53
+ 'Read the entire object to the ' \
54
+ 'end before you start using the decrypted data. This is to ' \
55
+ 'verify that the object has not been modified since it ' \
56
+ 'was encrypted.')
57
+ end
58
+
36
59
  @handler.call(context)
37
60
  end
38
61
 
@@ -41,9 +64,9 @@ module Aws
41
64
  def attach_http_event_listeners(context)
42
65
 
43
66
  context.http_response.on_headers(200) do
44
- cipher = decryption_cipher(context)
45
- decrypter = body_contains_auth_tag?(context) ?
46
- authenticated_decrypter(context, cipher) :
67
+ cipher, envelope = decryption_cipher(context)
68
+ decrypter = body_contains_auth_tag?(envelope) ?
69
+ authenticated_decrypter(context, cipher, envelope) :
47
70
  IODecrypter.new(cipher, context.http_response.body)
48
71
  context.http_response.body = decrypter
49
72
  end
@@ -63,8 +86,13 @@ module Aws
63
86
  end
64
87
 
65
88
  def decryption_cipher(context)
66
- if envelope = get_encryption_envelope(context)
67
- context[:encryption][:cipher_provider].decryption_cipher(envelope)
89
+ if (envelope = get_encryption_envelope(context))
90
+ cipher = context[:encryption][:cipher_provider]
91
+ .decryption_cipher(
92
+ envelope,
93
+ context[:encryption]
94
+ )
95
+ [cipher, envelope]
68
96
  else
69
97
  raise Errors::DecryptionError, "unable to locate encryption envelope"
70
98
  end
@@ -100,13 +128,12 @@ module Aws
100
128
  end
101
129
 
102
130
  def extract_envelope(hash)
131
+ return nil unless hash
103
132
  return v1_envelope(hash) if hash.key?('x-amz-key')
104
133
  return v2_envelope(hash) if hash.key?('x-amz-key-v2')
105
134
  if hash.keys.any? { |key| key.match(/^x-amz-key-(.+)$/) }
106
135
  msg = "unsupported envelope encryption version #{$1}"
107
136
  raise Errors::DecryptionError, msg
108
- else
109
- nil # no envelope found
110
137
  end
111
138
  end
112
139
 
@@ -120,39 +147,31 @@ module Aws
120
147
  msg = "unsupported content encrypting key (cek) format: #{alg}"
121
148
  raise Errors::DecryptionError, msg
122
149
  end
123
- unless envelope['x-amz-wrap-alg'] == 'kms'
124
- # possible to support
125
- # RSA/ECB/OAEPWithSHA-256AndMGF1Padding
150
+ unless POSSIBLE_WRAPPING_FORMATS.include? envelope['x-amz-wrap-alg']
126
151
  alg = envelope['x-amz-wrap-alg'].inspect
127
152
  msg = "unsupported key wrapping algorithm: #{alg}"
128
153
  raise Errors::DecryptionError, msg
129
154
  end
130
- unless V2_ENVELOPE_KEYS.sort == envelope.keys.sort
155
+ unless (missing_keys = V2_ENVELOPE_KEYS - envelope.keys).empty?
131
156
  msg = "incomplete v2 encryption envelope:\n"
132
- msg += " expected: #{V2_ENVELOPE_KEYS.join(',')}\n"
133
- msg += " got: #{envelope_keys.join(', ')}"
157
+ msg += " missing: #{missing_keys.join(',')}\n"
134
158
  raise Errors::DecryptionError, msg
135
159
  end
136
160
  envelope
137
161
  end
138
162
 
139
- # When the x-amz-meta-x-amz-tag-len header is present, it indicates
140
- # that the body of this object has a trailing auth tag. The header
141
- # indicates the length of that tag.
142
- #
143
163
  # This method fetches the tag from the end of the object by
144
164
  # making a GET Object w/range request. This auth tag is used
145
165
  # to initialize the cipher, and the decrypter truncates the
146
166
  # auth tag from the body when writing the final bytes.
147
- def authenticated_decrypter(context, cipher)
167
+ def authenticated_decrypter(context, cipher, envelope)
148
168
  if RUBY_VERSION.match(/1.9/)
149
- raise "authenticated decryption not supported by OpeenSSL in Ruby version ~> 1.9"
169
+ raise "authenticated decryption not supported by OpenSSL in Ruby version ~> 1.9"
150
170
  raise Aws::Errors::NonSupportedRubyVersionError, msg
151
171
  end
152
172
  http_resp = context.http_response
153
173
  content_length = http_resp.headers['content-length'].to_i
154
- auth_tag_length = http_resp.headers['x-amz-meta-x-amz-tag-len']
155
- auth_tag_length = auth_tag_length.to_i / 8
174
+ auth_tag_length = auth_tag_length(envelope)
156
175
 
157
176
  auth_tag = context.client.get_object(
158
177
  bucket: context.params[:bucket],
@@ -164,23 +183,39 @@ module Aws
164
183
  cipher.auth_data = ''
165
184
 
166
185
  # The encrypted object contains both the cipher text
167
- # plus a trailing auth tag. This decrypter will the body
168
- # expect for the trailing auth tag.
186
+ # plus a trailing auth tag.
169
187
  IOAuthDecrypter.new(
170
188
  io: http_resp.body,
171
189
  encrypted_content_length: content_length - auth_tag_length,
172
190
  cipher: cipher)
173
191
  end
174
192
 
175
- def body_contains_auth_tag?(context)
176
- context.http_response.headers['x-amz-meta-x-amz-tag-len']
193
+ def body_contains_auth_tag?(envelope)
194
+ AUTH_REQUIRED_CEK_ALGS.include?(envelope['x-amz-cek-alg'])
195
+ end
196
+
197
+ # Determine the auth tag length from the algorithm
198
+ # Validate it against the value provided in the x-amz-tag-len
199
+ # Return the tag length in bytes
200
+ def auth_tag_length(envelope)
201
+ tag_length =
202
+ case envelope['x-amz-cek-alg']
203
+ when 'AES/GCM/NoPadding' then AES_GCM_TAG_LEN_BYTES
204
+ else
205
+ raise ArgumentError, 'Unsupported cek-alg: ' \
206
+ "#{envelope['x-amz-cek-alg']}"
207
+ end
208
+ if (tag_length * 8) != envelope['x-amz-tag-len'].to_i
209
+ raise Errors::DecryptionError, 'x-amz-tag-len does not match expected'
210
+ end
211
+ tag_length
177
212
  end
178
213
 
179
214
  def apply_cse_user_agent(context)
180
215
  if context.config.user_agent_suffix.nil?
181
- context.config.user_agent_suffix = 'CSE_V1'
182
- elsif !context.config.user_agent_suffix.include? 'CSE_V1'
183
- context.config.user_agent_suffix += ' CSE_V1'
216
+ context.config.user_agent_suffix = EC_USER_AGENT
217
+ elsif !context.config.user_agent_suffix.include? EC_USER_AGENT
218
+ context.config.user_agent_suffix += " #{EC_USER_AGENT}"
184
219
  end
185
220
  end
186
221
 
@@ -26,11 +26,48 @@ module Aws
26
26
 
27
27
  # @return [Cipher] Given an encryption envelope, returns a
28
28
  # decryption cipher.
29
- def decryption_cipher(envelope)
29
+ def decryption_cipher(envelope, options = {})
30
30
  master_key = @key_provider.key_for(envelope['x-amz-matdesc'])
31
- key = Utils.decrypt(master_key, decode64(envelope['x-amz-key']))
32
- iv = decode64(envelope['x-amz-iv'])
33
- Utils.aes_decryption_cipher(:CBC, key, iv)
31
+ if envelope.key? 'x-amz-key'
32
+ # Support for decryption of legacy objects
33
+ key = Utils.decrypt(master_key, decode64(envelope['x-amz-key']))
34
+ iv = decode64(envelope['x-amz-iv'])
35
+ Utils.aes_decryption_cipher(:CBC, key, iv)
36
+ else
37
+ if envelope['x-amz-cek-alg'] != 'AES/GCM/NoPadding'
38
+ raise ArgumentError, 'Unsupported cek-alg: ' \
39
+ "#{envelope['x-amz-cek-alg']}"
40
+ end
41
+ key =
42
+ case envelope['x-amz-wrap-alg']
43
+ when 'AES/GCM'
44
+ if master_key.is_a? OpenSSL::PKey::RSA
45
+ raise ArgumentError, 'Key mismatch - Client is configured' \
46
+ ' with an RSA key and the x-amz-wrap-alg is AES/GCM.'
47
+ end
48
+ Utils.decrypt_aes_gcm(master_key,
49
+ decode64(envelope['x-amz-key-v2']),
50
+ envelope['x-amz-cek-alg'])
51
+ when 'RSA-OAEP-SHA1'
52
+ unless master_key.is_a? OpenSSL::PKey::RSA
53
+ raise ArgumentError, 'Key mismatch - Client is configured' \
54
+ ' with an AES key and the x-amz-wrap-alg is RSA-OAEP-SHA1.'
55
+ end
56
+ key, cek_alg = Utils.decrypt_rsa(master_key, decode64(envelope['x-amz-key-v2']))
57
+ raise Errors::DecryptionError unless cek_alg == envelope['x-amz-cek-alg']
58
+ key
59
+ when 'kms+context'
60
+ raise ArgumentError, 'Key mismatch - Client is configured' \
61
+ ' with a user provided key and the x-amz-wrap-alg is' \
62
+ ' kms+context. Please configure the client with the' \
63
+ ' required kms_key_id'
64
+ else
65
+ raise ArgumentError, 'Unsupported wrap-alg: ' \
66
+ "#{envelope['x-amz-wrap-alg']}"
67
+ end
68
+ iv = decode64(envelope['x-amz-iv'])
69
+ Utils.aes_decryption_cipher(:GCM, key, iv)
70
+ end
34
71
  end
35
72
 
36
73
  private
@@ -58,7 +95,6 @@ module Aws
58
95
  def decode64(str)
59
96
  Base64.decode64(str)
60
97
  end
61
-
62
98
  end
63
99
  end
64
100
  end
@@ -39,8 +39,8 @@ module Aws
39
39
  context.params[:body] = IOEncrypter.new(cipher, io)
40
40
  context.params[:metadata] ||= {}
41
41
  context.params[:metadata]['x-amz-unencrypted-content-length'] = io.size
42
- if md5 = context.params.delete(:content_md5)
43
- context.params[:metadata]['x-amz-unencrypted-content-md5'] = md5
42
+ if context.params.delete(:content_md5)
43
+ warn('Setting content_md5 on client side encrypted objects is deprecated')
44
44
  end
45
45
  context.http_response.on_headers do
46
46
  context.params[:body].close
@@ -49,9 +49,9 @@ module Aws
49
49
 
50
50
  def apply_cse_user_agent(context)
51
51
  if context.config.user_agent_suffix.nil?
52
- context.config.user_agent_suffix = 'CSE_V1'
53
- elsif !context.config.user_agent_suffix.include? 'CSE_V1'
54
- context.config.user_agent_suffix += ' CSE_V1'
52
+ context.config.user_agent_suffix = EC_USER_AGENT
53
+ elsif !context.config.user_agent_suffix.include? EC_USER_AGENT
54
+ context.config.user_agent_suffix += " #{EC_USER_AGENT}"
55
55
  end
56
56
  end
57
57
 
@@ -9,9 +9,10 @@ module Aws
9
9
  # @param [OpenSSL::Cipher] cipher
10
10
  # @param [IO#write] io An IO-like object that responds to `#write`.
11
11
  def initialize(cipher, io)
12
- @cipher = cipher.clone
12
+ @cipher = cipher
13
13
  # Ensure that IO is reset between retries
14
14
  @io = io.tap { |io| io.truncate(0) if io.respond_to?(:truncate) }
15
+ @cipher_buffer = String.new
15
16
  end
16
17
 
17
18
  # @return [#write]
@@ -19,17 +20,17 @@ module Aws
19
20
 
20
21
  def write(chunk)
21
22
  # decrypt and write
22
- @io.write(@cipher.update(chunk))
23
+ if @cipher.method(:update).arity == 1
24
+ @io.write(@cipher.update(chunk))
25
+ else
26
+ @io.write(@cipher.update(chunk, @cipher_buffer))
27
+ end
23
28
  end
24
29
 
25
30
  def finalize
26
31
  @io.write(@cipher.final)
27
32
  end
28
33
 
29
- def size
30
- @io.size
31
- end
32
-
33
34
  end
34
35
  end
35
36
  end
@@ -36,15 +36,36 @@ module Aws
36
36
 
37
37
  # @return [Cipher] Given an encryption envelope, returns a
38
38
  # decryption cipher.
39
- def decryption_cipher(envelope)
39
+ def decryption_cipher(envelope, options = {})
40
40
  encryption_context = Json.load(envelope['x-amz-matdesc'])
41
+ cek_alg = envelope['x-amz-cek-alg']
42
+
43
+ case envelope['x-amz-wrap-alg']
44
+ when 'kms'; # NO OP
45
+ when 'kms+context'
46
+ if cek_alg != encryption_context['aws:x-amz-cek-alg']
47
+ raise Errors::DecryptionError, 'Value of cek-alg from envelope'\
48
+ ' does not match the value in the encryption context'
49
+ end
50
+ when 'AES/GCM'
51
+ raise ArgumentError, 'Key mismatch - Client is configured' \
52
+ ' with a KMS key and the x-amz-wrap-alg is AES/GCM.'
53
+ when 'RSA-OAEP-SHA1'
54
+ raise ArgumentError, 'Key mismatch - Client is configured' \
55
+ ' with a KMS key and the x-amz-wrap-alg is RSA-OAEP-SHA1.'
56
+ else
57
+ raise ArgumentError, 'Unsupported wrap-alg: ' \
58
+ "#{envelope['x-amz-wrap-alg']}"
59
+ end
60
+
41
61
  key = @kms_client.decrypt(
42
62
  ciphertext_blob: decode64(envelope['x-amz-key-v2']),
43
- encryption_context: encryption_context,
63
+ encryption_context: encryption_context
44
64
  ).plaintext
65
+
45
66
  iv = decode64(envelope['x-amz-iv'])
46
67
  block_mode =
47
- case envelope['x-amz-cek-alg']
68
+ case cek_alg
48
69
  when 'AES/CBC/PKCS5Padding'
49
70
  :CBC
50
71
  when 'AES/CBC/PKCS7Padding'
@@ -61,6 +82,14 @@ module Aws
61
82
 
62
83
  private
63
84
 
85
+ def build_encryption_context(cek_alg, options = {})
86
+ kms_context = (options[:kms_encryption_context] || {})
87
+ .each_with_object({}) { |(k, v), h| h[k.to_s] = v }
88
+ {
89
+ 'aws:x-amz-cek-alg' => cek_alg
90
+ }.merge(kms_context)
91
+ end
92
+
64
93
  def encode64(str)
65
94
  Base64.encode64(str).split("\n") * ""
66
95
  end
@@ -39,6 +39,29 @@ module Aws
39
39
  end
40
40
  end
41
41
 
42
+
43
+ def decrypt_aes_gcm(key, data, auth_data)
44
+ # data is iv (12B) + key + tag (16B)
45
+ buf = data.unpack('C*')
46
+ iv = buf[0,12].pack('C*') # iv will always be 12 bytes
47
+ tag = buf[-16, 16].pack('C*') # tag is 16 bytes
48
+ enc_key = buf[12, buf.size - (12+16)].pack('C*')
49
+ cipher = aes_cipher(:decrypt, :GCM, key, iv)
50
+ cipher.auth_tag = tag
51
+ cipher.auth_data = auth_data
52
+ cipher.update(enc_key) + cipher.final
53
+ end
54
+
55
+ # returns the decrypted data + auth_data
56
+ def decrypt_rsa(key, enc_data)
57
+ # Plaintext must be KeyLengthInBytes (1 Byte) + DataKey + AuthData
58
+ buf = key.private_decrypt(enc_data, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING).unpack('C*')
59
+ key_length = buf[0]
60
+ data = buf[1, key_length].pack('C*')
61
+ auth_data = buf[key_length+1, buf.length - key_length].pack('C*')
62
+ [data, auth_data]
63
+ end
64
+
42
65
  # @param [String] block_mode "CBC" or "ECB"
43
66
  # @param [OpenSSL::PKey::RSA, String, nil] key
44
67
  # @param [String, nil] iv The initialization vector