aws-sdk-s3 1.207.0 → 1.208.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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +5 -0
  3. data/VERSION +1 -1
  4. data/lib/aws-sdk-s3/client.rb +1 -1
  5. data/lib/aws-sdk-s3/customizations.rb +1 -0
  6. data/lib/aws-sdk-s3/encryption/client.rb +2 -2
  7. data/lib/aws-sdk-s3/encryption/default_cipher_provider.rb +2 -0
  8. data/lib/aws-sdk-s3/encryption/encrypt_handler.rb +2 -0
  9. data/lib/aws-sdk-s3/encryption/kms_cipher_provider.rb +2 -0
  10. data/lib/aws-sdk-s3/encryptionV2/client.rb +98 -23
  11. data/lib/aws-sdk-s3/encryptionV2/decrypt_handler.rb +7 -162
  12. data/lib/aws-sdk-s3/encryptionV2/decryption.rb +205 -0
  13. data/lib/aws-sdk-s3/encryptionV2/default_cipher_provider.rb +17 -0
  14. data/lib/aws-sdk-s3/encryptionV2/encrypt_handler.rb +2 -0
  15. data/lib/aws-sdk-s3/encryptionV2/io_encrypter.rb +2 -0
  16. data/lib/aws-sdk-s3/encryptionV2/kms_cipher_provider.rb +8 -0
  17. data/lib/aws-sdk-s3/encryptionV2/utils.rb +5 -0
  18. data/lib/aws-sdk-s3/encryptionV3/client.rb +885 -0
  19. data/lib/aws-sdk-s3/encryptionV3/decrypt_handler.rb +98 -0
  20. data/lib/aws-sdk-s3/encryptionV3/decryption.rb +244 -0
  21. data/lib/aws-sdk-s3/encryptionV3/default_cipher_provider.rb +159 -0
  22. data/lib/aws-sdk-s3/encryptionV3/default_key_provider.rb +35 -0
  23. data/lib/aws-sdk-s3/encryptionV3/encrypt_handler.rb +98 -0
  24. data/lib/aws-sdk-s3/encryptionV3/errors.rb +47 -0
  25. data/lib/aws-sdk-s3/encryptionV3/io_auth_decrypter.rb +60 -0
  26. data/lib/aws-sdk-s3/encryptionV3/io_decrypter.rb +35 -0
  27. data/lib/aws-sdk-s3/encryptionV3/io_encrypter.rb +84 -0
  28. data/lib/aws-sdk-s3/encryptionV3/key_provider.rb +28 -0
  29. data/lib/aws-sdk-s3/encryptionV3/kms_cipher_provider.rb +159 -0
  30. data/lib/aws-sdk-s3/encryptionV3/materials.rb +58 -0
  31. data/lib/aws-sdk-s3/encryptionV3/utils.rb +321 -0
  32. data/lib/aws-sdk-s3/encryption_v2.rb +1 -0
  33. data/lib/aws-sdk-s3/encryption_v3.rb +24 -0
  34. data/lib/aws-sdk-s3.rb +1 -1
  35. metadata +17 -1
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aws
4
+ module S3
5
+ module EncryptionV3
6
+ module Errors
7
+ # Generic DecryptionError
8
+ class DecryptionError < RuntimeError; end
9
+
10
+ class EncryptionError < RuntimeError; end
11
+
12
+ # Raised when attempting to decrypt a legacy (V1) encrypted object
13
+ # when using a security_profile that does not support it.
14
+ class NonCommittingDecryptionError < DecryptionError
15
+ def initialize(*_args)
16
+ msg = 'The requested object is ' \
17
+ 'was not encrypted with a committing algorithm ' \
18
+ 'and decryption is not supported under :require_encrypt_require_decrypt commitment policy. ' \
19
+ 'Change your commitment policy to :forbid_encrypt_allow_decrypt or :require_encrypt_allow_decrypt'
20
+ super(msg)
21
+ end
22
+ end
23
+
24
+ # Raised when attempting to decrypt a legacy (V1) encrypted object
25
+ # when using a security_profile that does not support it.
26
+ class LegacyDecryptionError < DecryptionError
27
+ def initialize(*_args)
28
+ msg = 'The requested object is ' \
29
+ 'encrypted with V1 encryption schemas that have been disabled ' \
30
+ 'by client configuration security_profile = :v2. Retry with ' \
31
+ ':v2_and_legacy or re-encrypt the object.'
32
+ super(msg)
33
+ end
34
+ end
35
+
36
+ class CEKAlgMismatchError < DecryptionError
37
+ def initialize(*_args)
38
+ msg = 'The content encryption algorithm used at encryption time ' \
39
+ 'does not match the algorithm stored for decryption time. ' \
40
+ 'The object may be altered or corrupted.'
41
+ super(msg)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aws
4
+ module S3
5
+ module EncryptionV3
6
+ # @api private
7
+ class IOAuthDecrypter
8
+ # @option options [required, IO#write] :io
9
+ # An IO-like object that responds to {#write}.
10
+ # @option options [required, Integer] :encrypted_content_length
11
+ # The number of bytes to decrypt from the `:io` object.
12
+ # This should be the total size of `:io` minus the length of
13
+ # the cipher auth tag.
14
+ # @option options [required, OpenSSL::Cipher] :cipher An initialized
15
+ # cipher that can be used to decrypt the bytes as they are
16
+ # written to the `:io` object.
17
+ def initialize(options = {})
18
+ @decrypter = IODecrypter.new(options[:cipher], options[:io])
19
+ @max_bytes = options[:encrypted_content_length]
20
+ @bytes_written = 0
21
+ @cipher = options[:cipher]
22
+ @auth_tag = String.new
23
+ end
24
+
25
+ def write(chunk)
26
+ chunk = truncate_chunk(chunk)
27
+ return unless chunk.bytesize.positive?
28
+
29
+ @bytes_written += chunk.bytesize
30
+ @decrypter.write(chunk)
31
+ end
32
+
33
+ def finalize
34
+ @cipher.auth_tag = @auth_tag
35
+ @decrypter.finalize
36
+ end
37
+
38
+ def io
39
+ @decrypter.io
40
+ end
41
+
42
+ private
43
+
44
+ def truncate_chunk(chunk)
45
+ if chunk.bytesize + @bytes_written <= @max_bytes
46
+ chunk
47
+ elsif @bytes_written < @max_bytes
48
+ @auth_tag << chunk[@max_bytes - @bytes_written..-1]
49
+ chunk[0..(@max_bytes - @bytes_written - 1)]
50
+ else
51
+ @auth_tag << chunk
52
+ # If the tag was sent over after the full body has been read,
53
+ # we don't want to accidentally append it.
54
+ ''
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aws
4
+ module S3
5
+ module EncryptionV3
6
+ # @api private
7
+ class IODecrypter
8
+ # @param [OpenSSL::Cipher] cipher
9
+ # @param [IO#write] io An IO-like object that responds to `#write`.
10
+ def initialize(cipher, io)
11
+ @cipher = cipher
12
+ # Ensure that IO is reset between retries
13
+ @io = io.tap { |io| io.truncate(0) if io.respond_to?(:truncate) }
14
+ @cipher_buffer = String.new
15
+ end
16
+
17
+ # @return [#write]
18
+ attr_reader :io
19
+
20
+ def write(chunk)
21
+ # decrypt and write
22
+ if @cipher.method(:update).arity == 1
23
+ @io.write(@cipher.update(chunk))
24
+ else
25
+ @io.write(@cipher.update(chunk, @cipher_buffer))
26
+ end
27
+ end
28
+
29
+ def finalize
30
+ @io.write(@cipher.final)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stringio'
4
+ require 'tempfile'
5
+
6
+ module Aws
7
+ module S3
8
+ module EncryptionV3
9
+ # Provides an IO wrapper encrypting a stream of data.
10
+ # @api private
11
+ class IOEncrypter
12
+ # @api private
13
+ ONE_MEGABYTE = 1024 * 1024
14
+
15
+ def initialize(cipher, io)
16
+ @encrypted = if io.size <= ONE_MEGABYTE
17
+ encrypt_to_stringio(cipher, io.read)
18
+ else
19
+ encrypt_to_tempfile(cipher, io)
20
+ end
21
+ @size = @encrypted.size
22
+ end
23
+
24
+ # @return [Integer]
25
+ attr_reader :size
26
+
27
+ def read(bytes = nil, output_buffer = nil)
28
+ if @encrypted.is_a?(Tempfile) && @encrypted.closed?
29
+ @encrypted.open
30
+ @encrypted.binmode
31
+ end
32
+ @encrypted.read(bytes, output_buffer)
33
+ end
34
+
35
+ def rewind
36
+ @encrypted.rewind
37
+ end
38
+
39
+ # @api private
40
+ def close
41
+ @encrypted.close if @encrypted.is_a?(Tempfile)
42
+ end
43
+
44
+ private
45
+
46
+ ##= ../specification/s3-encryption/encryption.md#alg-aes-256-gcm-hkdf-sha512-commit-key
47
+ ##% The client MUST append the GCM auth tag to the ciphertext if the underlying crypto provider does not do so automatically.
48
+
49
+ def encrypt_to_stringio(cipher, plain_text)
50
+ if plain_text.empty?
51
+ StringIO.new(cipher.final + cipher.auth_tag)
52
+ else
53
+ StringIO.new(cipher.update(plain_text) + cipher.final + cipher.auth_tag)
54
+ end
55
+ end
56
+
57
+ def encrypt_to_tempfile(cipher, io)
58
+ encrypted = Tempfile.new(object_id.to_s)
59
+ encrypted.binmode
60
+ ##= ../specification/s3-encryption/encryption.md#content-encryption
61
+ ##= type=implication
62
+ ##% The client MUST validate that the length of the plaintext bytes does not exceed the algorithm suite's cipher's maximum content length in bytes.
63
+ # The expectation is that this is handled by the underlying cryptographic provider.
64
+ # In Ruby this is OpenSSL by default.
65
+ # See OpenSSL: https://github.com/openssl/openssl/blob/master/crypto/modes/gcm128.c#L784
66
+ # The relevant line is:
67
+ # if (mlen > ((U64(1) << 36) - 32) || (sizeof(len) == 8 && mlen < len))
68
+ # return -1;
69
+ while (chunk = io.read(ONE_MEGABYTE, read_buffer ||= String.new))
70
+ if cipher.method(:update).arity == 1
71
+ encrypted.write(cipher.update(chunk))
72
+ else
73
+ encrypted.write(cipher.update(chunk, cipher_buffer ||= String.new))
74
+ end
75
+ end
76
+ encrypted.write(cipher.final)
77
+ encrypted.write(cipher.auth_tag)
78
+ encrypted.rewind
79
+ encrypted
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aws
4
+ module S3
5
+ module EncryptionV3
6
+ # This module defines the interface required for a {Client#key_provider}.
7
+ # A key provider is any object that:
8
+ #
9
+ # * Responds to {#encryption_materials} with an {Materials} object.
10
+ #
11
+ # * Responds to {#key_for}, receiving a JSON document String,
12
+ # returning an encryption key. The returned encryption key
13
+ # must be one of:
14
+ #
15
+ # * `OpenSSL::PKey::RSA` - for asymmetric encryption
16
+ # * `String` - 32, 24, or 16 bytes long, for symmetric encryption
17
+ #
18
+ module KeyProvider
19
+ # @return [Materials]
20
+ def encryption_materials; end
21
+
22
+ # @param [String<JSON>] materials_description
23
+ # @return [OpenSSL::PKey::RSA, String] encryption_key
24
+ def key_for(materials_description); end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+
5
+ module Aws
6
+ module S3
7
+ module EncryptionV3
8
+ # @api private
9
+ class KmsCipherProvider
10
+ def initialize(options = {})
11
+ @kms_key_id = validate_kms_key(options[:kms_key_id])
12
+ @kms_client = options[:kms_client]
13
+ @key_wrap_schema = validate_key_wrap(
14
+ options[:key_wrap_schema]
15
+ )
16
+ @content_encryption_schema = Utils.validate_cek(
17
+ options[:content_encryption_schema]
18
+ )
19
+ end
20
+
21
+ # @return [Array<Hash,Cipher>] Creates and returns a new encryption
22
+ # envelope and encryption cipher.
23
+ def encryption_cipher(options = {})
24
+ validate_key_for_encryption
25
+ encryption_context = build_encryption_context(@content_encryption_schema, options)
26
+ key_data = Aws::Plugins::UserAgent.metric('S3_CRYPTO_V3') do
27
+ @kms_client.generate_data_key(
28
+ key_id: @kms_key_id,
29
+ encryption_context: encryption_context,
30
+ key_spec: 'AES_256'
31
+ )
32
+ end
33
+ cipher, message_id, commitment_key = Utils.generate_alg_aes_256_gcm_hkdf_sha512_commit_key_cipher(key_data.plaintext)
34
+ ##= ../specification/s3-encryption/data-format/content-metadata.md#algorithm-suite-and-message-format-version-compatibility
35
+ ##% Objects encrypted with ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY MUST use the V3 message format version only.
36
+ envelope = {
37
+ 'x-amz-3' => encode64(key_data.ciphertext_blob),
38
+ 'x-amz-c' => @content_encryption_schema,
39
+ 'x-amz-w' => @key_wrap_schema,
40
+ 'x-amz-d' => encode64(commitment_key),
41
+ 'x-amz-i' => encode64(message_id),
42
+ ##= ../specification/s3-encryption/data-format/content-metadata.md#v3-only
43
+ ##% The Encryption Context value MUST be used for wrapping algorithm `kms+context` or `12`.
44
+ 'x-amz-t' => Json.dump(encryption_context)
45
+ }
46
+ [envelope, cipher]
47
+ end
48
+
49
+ # @return [Cipher] Given an encryption envelope, returns a
50
+ # decryption cipher.
51
+ def decryption_cipher(envelope, options = {})
52
+ case envelope['x-amz-w']
53
+ when '12'
54
+ cek_alg = envelope['x-amz-c']
55
+ encryption_context =
56
+ if !envelope['x-amz-t'].nil?
57
+ Json.load(envelope['x-amz-t'])
58
+ else
59
+ ##= ../specification/s3-encryption/data-format/content-metadata.md#v3-only
60
+ ##% If the mapkey x-amz-t is not present, the default Material Description value MUST be set to an empty map (`{}`).
61
+ {}
62
+ end
63
+ ##= ../specification/s3-encryption/data-format/content-metadata.md#v3-only
64
+ ##% - The wrapping algorithm value "12" MUST be translated to kms+context upon retrieval, and vice versa on write.
65
+ raise Errors::CEKAlgMismatchError if cek_alg != encryption_context['aws:x-amz-cek-alg']
66
+
67
+ if encryption_context != build_encryption_context(cek_alg, options)
68
+ raise Errors::DecryptionError, 'Value of encryption context from'\
69
+ ' envelope does not match the provided encryption context'
70
+ end
71
+ when '02'
72
+ raise ArgumentError, 'Key mismatch - Client is configured' \
73
+ ' with a KMS key and the x-amz-wrap-alg is AES/GCM.'
74
+ when '22'
75
+ raise ArgumentError, 'Key mismatch - Client is configured' \
76
+ ' with a KMS key and the x-amz-wrap-alg is RSA-OAEP-SHA1.'
77
+ when nil
78
+ raise ArgumentError, 'Plaintext passthrough not supported'
79
+ else
80
+ # assert !envelope['x-amz-w'].nil?
81
+ # because of the when above
82
+ raise ArgumentError, 'Unsupported wrapping algorithm: ' \
83
+ "#{envelope['x-amz-w']}"
84
+ end
85
+
86
+ any_cmk_mode = options[:kms_allow_decrypt_with_any_cmk]
87
+ decrypt_options = {
88
+ ciphertext_blob: decode64(envelope['x-amz-3']),
89
+ encryption_context: encryption_context
90
+ }
91
+ decrypt_options[:key_id] = @kms_key_id unless any_cmk_mode
92
+
93
+ data_key = Aws::Plugins::UserAgent.metric('S3_CRYPTO_V3') do
94
+ @kms_client.decrypt(decrypt_options).plaintext
95
+ end
96
+
97
+ message_id = decode64(envelope['x-amz-i'])
98
+ commitment_key = decode64(envelope['x-amz-d'])
99
+
100
+ Utils.derive_alg_aes_256_gcm_hkdf_sha512_commit_key_cipher(data_key, message_id, commitment_key)
101
+ end
102
+
103
+ private
104
+
105
+ def validate_key_wrap(key_wrap_schema)
106
+ case key_wrap_schema
107
+ when :kms_context then '12'
108
+ else
109
+ raise ArgumentError, "Unsupported key_wrap_schema: #{key_wrap_schema}"
110
+ end
111
+ end
112
+
113
+ def validate_kms_key(kms_key_id)
114
+ if kms_key_id.nil? || kms_key_id.empty?
115
+ raise ArgumentError, 'KMS CMK ID was not specified. ' \
116
+ 'Please specify a CMK ID, ' \
117
+ 'or set kms_key_id: :kms_allow_decrypt_with_any_cmk to use ' \
118
+ 'any valid CMK from the object.'
119
+ end
120
+
121
+ if kms_key_id.is_a?(Symbol) && kms_key_id != :kms_allow_decrypt_with_any_cmk
122
+ raise ArgumentError, 'kms_key_id must be a valid KMS CMK or be ' \
123
+ 'set to :kms_allow_decrypt_with_any_cmk'
124
+ end
125
+ kms_key_id
126
+ end
127
+
128
+ def build_encryption_context(cek_alg, options = {})
129
+ kms_context = (options[:kms_encryption_context] || {})
130
+ .transform_keys(&:to_s)
131
+ if kms_context.include? 'aws:x-amz-cek-alg'
132
+ raise ArgumentError, 'Conflict in reserved KMS Encryption Context ' \
133
+ 'key aws:x-amz-cek-alg. This value is reserved for the S3 ' \
134
+ 'Encryption Client and cannot be set by the user.'
135
+ end
136
+ {
137
+ 'aws:x-amz-cek-alg' => cek_alg
138
+ }.merge(kms_context)
139
+ end
140
+
141
+ def encode64(str)
142
+ Base64.encode64(str).split("\n") * ''
143
+ end
144
+
145
+ def decode64(str)
146
+ Base64.decode64(str)
147
+ end
148
+
149
+ def validate_key_for_encryption
150
+ return unless @kms_key_id == :kms_allow_decrypt_with_any_cmk
151
+
152
+ raise ArgumentError, 'Unable to encrypt/write objects with '\
153
+ 'kms_key_id = :kms_allow_decrypt_with_any_cmk. Provide ' \
154
+ 'a valid kms_key_id on client construction.'
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+
5
+ module Aws
6
+ module S3
7
+ module EncryptionV3
8
+ class Materials
9
+ # @option options [required, OpenSSL::PKey::RSA, String] :key
10
+ # The master key to use for encrypting/decrypting all objects.
11
+ #
12
+ # @option options [String<JSON>] :description ('{}')
13
+ # The encryption materials description. This is must be
14
+ # a JSON document string.
15
+ #
16
+ def initialize(options = {})
17
+ @key = validate_key(options[:key])
18
+ @description = validate_desc(options[:description])
19
+ end
20
+
21
+ # @return [OpenSSL::PKey::RSA, String]
22
+ attr_reader :key
23
+
24
+ # @return [String<JSON>]
25
+ attr_reader :description
26
+
27
+ private
28
+
29
+ def validate_key(key)
30
+ case key
31
+ when OpenSSL::PKey::RSA then key
32
+ when String
33
+ if [32, 24, 16].include?(key.bytesize)
34
+ key
35
+ else
36
+ msg = 'invalid key, symmetric key required to be 16, 24, or '\
37
+ '32 bytes in length, saw length ' + key.bytesize.to_s
38
+ raise ArgumentError, msg
39
+ end
40
+ else
41
+ msg = 'invalid encryption key, expected an OpenSSL::PKey::RSA key '\
42
+ '(for asymmetric encryption) or a String (for symmetric '\
43
+ 'encryption).'
44
+ raise ArgumentError, msg
45
+ end
46
+ end
47
+
48
+ def validate_desc(description)
49
+ Json.load(description)
50
+ description
51
+ rescue Json::ParseError, EncodingError
52
+ msg = 'expected description to be a valid JSON document string'
53
+ raise ArgumentError, msg
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end