aws-sdk-resources 2.11.557 → 2.11.562

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 (31) hide show
  1. checksums.yaml +4 -4
  2. data/lib/aws-sdk-resources/services/s3.rb +1 -0
  3. data/lib/aws-sdk-resources/services/s3/encryption.rb +3 -0
  4. data/lib/aws-sdk-resources/services/s3/encryption/client.rb +24 -7
  5. data/lib/aws-sdk-resources/services/s3/encryption/decrypt_handler.rb +65 -25
  6. data/lib/aws-sdk-resources/services/s3/encryption/default_cipher_provider.rb +43 -5
  7. data/lib/aws-sdk-resources/services/s3/encryption/default_key_provider.rb +2 -0
  8. data/lib/aws-sdk-resources/services/s3/encryption/encrypt_handler.rb +13 -2
  9. data/lib/aws-sdk-resources/services/s3/encryption/errors.rb +2 -0
  10. data/lib/aws-sdk-resources/services/s3/encryption/io_auth_decrypter.rb +2 -0
  11. data/lib/aws-sdk-resources/services/s3/encryption/io_decrypter.rb +8 -1
  12. data/lib/aws-sdk-resources/services/s3/encryption/io_encrypter.rb +2 -0
  13. data/lib/aws-sdk-resources/services/s3/encryption/key_provider.rb +2 -0
  14. data/lib/aws-sdk-resources/services/s3/encryption/kms_cipher_provider.rb +36 -3
  15. data/lib/aws-sdk-resources/services/s3/encryption/materials.rb +8 -6
  16. data/lib/aws-sdk-resources/services/s3/encryption/utils.rb +25 -0
  17. data/lib/aws-sdk-resources/services/s3/encryptionV2/client.rb +559 -0
  18. data/lib/aws-sdk-resources/services/s3/encryptionV2/decrypt_handler.rb +214 -0
  19. data/lib/aws-sdk-resources/services/s3/encryptionV2/default_cipher_provider.rb +170 -0
  20. data/lib/aws-sdk-resources/services/s3/encryptionV2/default_key_provider.rb +40 -0
  21. data/lib/aws-sdk-resources/services/s3/encryptionV2/encrypt_handler.rb +69 -0
  22. data/lib/aws-sdk-resources/services/s3/encryptionV2/errors.rb +37 -0
  23. data/lib/aws-sdk-resources/services/s3/encryptionV2/io_auth_decrypter.rb +58 -0
  24. data/lib/aws-sdk-resources/services/s3/encryptionV2/io_decrypter.rb +37 -0
  25. data/lib/aws-sdk-resources/services/s3/encryptionV2/io_encrypter.rb +73 -0
  26. data/lib/aws-sdk-resources/services/s3/encryptionV2/key_provider.rb +31 -0
  27. data/lib/aws-sdk-resources/services/s3/encryptionV2/kms_cipher_provider.rb +169 -0
  28. data/lib/aws-sdk-resources/services/s3/encryptionV2/materials.rb +60 -0
  29. data/lib/aws-sdk-resources/services/s3/encryptionV2/utils.rb +103 -0
  30. data/lib/aws-sdk-resources/services/s3/encryption_v2.rb +24 -0
  31. metadata +18 -4
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aws
4
+ module S3
5
+ module EncryptionV2
6
+ module Errors
7
+
8
+ # Generic DecryptionError
9
+ class DecryptionError < RuntimeError; end
10
+
11
+ class EncryptionError < RuntimeError; end
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
+
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aws
4
+ module S3
5
+ module EncryptionV2
6
+ # @api private
7
+ class IOAuthDecrypter
8
+
9
+ # @option options [required, IO#write] :io
10
+ # An IO-like object that responds to {#write}.
11
+ # @option options [required, Integer] :encrypted_content_length
12
+ # The number of bytes to decrypt from the `:io` object.
13
+ # This should be the total size of `:io` minus the length of
14
+ # the cipher auth tag.
15
+ # @option options [required, OpenSSL::Cipher] :cipher An initialized
16
+ # cipher that can be used to decrypt the bytes as they are
17
+ # written to the `:io` object. The cipher should already have
18
+ # its `#auth_tag` set.
19
+ def initialize(options = {})
20
+ @decrypter = IODecrypter.new(options[:cipher], options[:io])
21
+ @max_bytes = options[:encrypted_content_length]
22
+ @bytes_written = 0
23
+ end
24
+
25
+ def write(chunk)
26
+ chunk = truncate_chunk(chunk)
27
+ if chunk.bytesize > 0
28
+ @bytes_written += chunk.bytesize
29
+ @decrypter.write(chunk)
30
+ end
31
+ end
32
+
33
+ def finalize
34
+ @decrypter.finalize
35
+ end
36
+
37
+ def io
38
+ @decrypter.io
39
+ end
40
+
41
+ private
42
+
43
+ def truncate_chunk(chunk)
44
+ if chunk.bytesize + @bytes_written <= @max_bytes
45
+ chunk
46
+ elsif @bytes_written < @max_bytes
47
+ chunk[0..(@max_bytes - @bytes_written - 1)]
48
+ else
49
+ # If the tag was sent over after the full body has been read,
50
+ # we don't want to accidentally append it.
51
+ ""
52
+ end
53
+ end
54
+
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aws
4
+ module S3
5
+ module EncryptionV2
6
+ # @api private
7
+ class IODecrypter
8
+
9
+ # @param [OpenSSL::Cipher] cipher
10
+ # @param [IO#write] io An IO-like object that responds to `#write`.
11
+ def initialize(cipher, io)
12
+ @cipher = cipher
13
+ # Ensure that IO is reset between retries
14
+ @io = io.tap { |io| io.truncate(0) if io.respond_to?(:truncate) }
15
+ @cipher_buffer = String.new
16
+ end
17
+
18
+ # @return [#write]
19
+ attr_reader :io
20
+
21
+ def write(chunk)
22
+ # decrypt and write
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
28
+ end
29
+
30
+ def finalize
31
+ @io.write(@cipher.final)
32
+ end
33
+
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stringio'
4
+ require 'tempfile'
5
+
6
+ module Aws
7
+ module S3
8
+ module EncryptionV2
9
+
10
+ # Provides an IO wrapper encrypting a stream of data.
11
+ # @api private
12
+ class IOEncrypter
13
+
14
+ # @api private
15
+ ONE_MEGABYTE = 1024 * 1024
16
+
17
+ def initialize(cipher, io)
18
+ @encrypted = io.size <= ONE_MEGABYTE ?
19
+ encrypt_to_stringio(cipher, io.read) :
20
+ encrypt_to_tempfile(cipher, io)
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
+ def encrypt_to_stringio(cipher, plain_text)
47
+ if plain_text.empty?
48
+ StringIO.new(cipher.final + cipher.auth_tag)
49
+ else
50
+ StringIO.new(cipher.update(plain_text) + cipher.final + cipher.auth_tag)
51
+ end
52
+ end
53
+
54
+ def encrypt_to_tempfile(cipher, io)
55
+ encrypted = Tempfile.new(self.object_id.to_s)
56
+ encrypted.binmode
57
+ while chunk = io.read(ONE_MEGABYTE, read_buffer ||= String.new)
58
+ if cipher.method(:update).arity == 1
59
+ encrypted.write(cipher.update(chunk))
60
+ else
61
+ encrypted.write(cipher.update(chunk, cipher_buffer ||= String.new))
62
+ end
63
+ end
64
+ encrypted.write(cipher.final)
65
+ encrypted.write(cipher.auth_tag)
66
+ encrypted.rewind
67
+ encrypted
68
+ end
69
+
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aws
4
+ module S3
5
+ module EncryptionV2
6
+
7
+ # This module defines the interface required for a {Client#key_provider}.
8
+ # A key provider is any object that:
9
+ #
10
+ # * Responds to {#encryption_materials} with an {Materials} object.
11
+ #
12
+ # * Responds to {#key_for}, receiving a JSON document String,
13
+ # returning an encryption key. The returned encryption key
14
+ # must be one of:
15
+ #
16
+ # * `OpenSSL::PKey::RSA` - for asymmetric encryption
17
+ # * `String` - 32, 24, or 16 bytes long, for symmetric encryption
18
+ #
19
+ module KeyProvider
20
+
21
+ # @return [Materials]
22
+ def encryption_materials; end
23
+
24
+ # @param [String<JSON>] materials_description
25
+ # @return [OpenSSL::PKey::RSA, String] encryption_key
26
+ def key_for(materials_description); end
27
+
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+
5
+ module Aws
6
+ module S3
7
+ module EncryptionV2
8
+ # @api private
9
+ class KmsCipherProvider
10
+
11
+ def initialize(options = {})
12
+ @kms_key_id = validate_kms_key(options[:kms_key_id])
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
+ )
20
+ end
21
+
22
+ # @return [Array<Hash,Cipher>] Creates and returns a new encryption
23
+ # envelope and encryption cipher.
24
+ def encryption_cipher(options = {})
25
+ validate_key_for_encryption
26
+ encryption_context = build_encryption_context(@content_encryption_schema, options)
27
+ key_data = @kms_client.generate_data_key(
28
+ key_id: @kms_key_id,
29
+ encryption_context: encryption_context,
30
+ key_spec: 'AES_256'
31
+ )
32
+ cipher = Utils.aes_encryption_cipher(:GCM)
33
+ cipher.key = key_data.plaintext
34
+ envelope = {
35
+ 'x-amz-key-v2' => encode64(key_data.ciphertext_blob),
36
+ 'x-amz-iv' => encode64(cipher.iv = cipher.random_iv),
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,
40
+ 'x-amz-matdesc' => Json.dump(encryption_context)
41
+ }
42
+ cipher.auth_data = '' # auth_data must be set after key and iv
43
+ [envelope, cipher]
44
+ end
45
+
46
+ # @return [Cipher] Given an encryption envelope, returns a
47
+ # decryption cipher.
48
+ def decryption_cipher(envelope, options = {})
49
+ encryption_context = Json.load(envelope['x-amz-matdesc'])
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
61
+
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']}"
75
+ end
76
+
77
+ any_cmk_mode = false || options[:kms_allow_decrypt_with_any_cmk]
78
+ decrypt_options = {
79
+ ciphertext_blob: decode64(envelope['x-amz-key-v2']),
80
+ encryption_context: encryption_context
81
+ }
82
+ unless any_cmk_mode
83
+ decrypt_options[:key_id] = @kms_key_id
84
+ end
85
+
86
+ key = @kms_client.decrypt(decrypt_options).plaintext
87
+ iv = decode64(envelope['x-amz-iv'])
88
+ block_mode =
89
+ case cek_alg
90
+ when 'AES/CBC/PKCS5Padding'
91
+ :CBC
92
+ when 'AES/CBC/PKCS7Padding'
93
+ :CBC
94
+ when 'AES/GCM/NoPadding'
95
+ :GCM
96
+ else
97
+ type = envelope['x-amz-cek-alg'].inspect
98
+ msg = "unsupported content encrypting key (cek) format: #{type}"
99
+ raise Errors::DecryptionError, msg
100
+ end
101
+ Utils.aes_decryption_cipher(block_mode, key, iv)
102
+ end
103
+
104
+ private
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
+
138
+ def build_encryption_context(cek_alg, options = {})
139
+ kms_context = (options[:kms_encryption_context] || {})
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
146
+ {
147
+ 'aws:x-amz-cek-alg' => cek_alg
148
+ }.merge(kms_context)
149
+ end
150
+
151
+ def encode64(str)
152
+ Base64.encode64(str).split("\n") * ""
153
+ end
154
+
155
+ def decode64(str)
156
+ Base64.decode64(str)
157
+ end
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
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+
5
+ module Aws
6
+ module S3
7
+ module EncryptionV2
8
+ class Materials
9
+
10
+ # @option options [required, OpenSSL::PKey::RSA, String] :key
11
+ # The master key to use for encrypting/decrypting all objects.
12
+ #
13
+ # @option options [String<JSON>] :description ('{}')
14
+ # The encryption materials description. This is must be
15
+ # a JSON document string.
16
+ #
17
+ def initialize(options = {})
18
+ @key = validate_key(options[:key])
19
+ @description = validate_desc(options[:description])
20
+ end
21
+
22
+ # @return [OpenSSL::PKey::RSA, String]
23
+ attr_reader :key
24
+
25
+ # @return [String<JSON>]
26
+ attr_reader :description
27
+
28
+ private
29
+
30
+ def validate_key(key)
31
+ case key
32
+ when OpenSSL::PKey::RSA then key
33
+ when String
34
+ if [32, 24, 16].include?(key.bytesize)
35
+ key
36
+ else
37
+ msg = 'invalid key, symmetric key required to be 16, 24, or '\
38
+ '32 bytes in length, saw length ' + key.bytesize.to_s
39
+ raise ArgumentError, msg
40
+ end
41
+ else
42
+ msg = 'invalid encryption key, expected an OpenSSL::PKey::RSA key '\
43
+ '(for asymmetric encryption) or a String (for symmetric '\
44
+ 'encryption).'
45
+ raise ArgumentError, msg
46
+ end
47
+ end
48
+
49
+ def validate_desc(description)
50
+ Json.load(description)
51
+ description
52
+ rescue Json::ParseError, EncodingError
53
+ msg = 'expected description to be a valid JSON document string'
54
+ raise ArgumentError, msg
55
+ end
56
+
57
+ end
58
+ end
59
+ end
60
+ end