aws-sdk-resources 2.11.558 → 2.11.563
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.
- checksums.yaml +4 -4
- data/lib/aws-sdk-resources/services/s3.rb +1 -0
- data/lib/aws-sdk-resources/services/s3/encryption.rb +3 -0
- data/lib/aws-sdk-resources/services/s3/encryption/client.rb +24 -7
- data/lib/aws-sdk-resources/services/s3/encryption/decrypt_handler.rb +77 -26
- data/lib/aws-sdk-resources/services/s3/encryption/default_cipher_provider.rb +43 -5
- data/lib/aws-sdk-resources/services/s3/encryption/default_key_provider.rb +2 -0
- data/lib/aws-sdk-resources/services/s3/encryption/encrypt_handler.rb +13 -2
- data/lib/aws-sdk-resources/services/s3/encryption/errors.rb +2 -0
- data/lib/aws-sdk-resources/services/s3/encryption/io_auth_decrypter.rb +2 -0
- data/lib/aws-sdk-resources/services/s3/encryption/io_decrypter.rb +11 -3
- data/lib/aws-sdk-resources/services/s3/encryption/io_encrypter.rb +2 -0
- data/lib/aws-sdk-resources/services/s3/encryption/key_provider.rb +2 -0
- data/lib/aws-sdk-resources/services/s3/encryption/kms_cipher_provider.rb +36 -3
- data/lib/aws-sdk-resources/services/s3/encryption/materials.rb +8 -6
- data/lib/aws-sdk-resources/services/s3/encryption/utils.rb +25 -0
- data/lib/aws-sdk-resources/services/s3/encryptionV2/client.rb +561 -0
- data/lib/aws-sdk-resources/services/s3/encryptionV2/decrypt_handler.rb +214 -0
- data/lib/aws-sdk-resources/services/s3/encryptionV2/default_cipher_provider.rb +170 -0
- data/lib/aws-sdk-resources/services/s3/encryptionV2/default_key_provider.rb +40 -0
- data/lib/aws-sdk-resources/services/s3/encryptionV2/encrypt_handler.rb +69 -0
- data/lib/aws-sdk-resources/services/s3/encryptionV2/errors.rb +37 -0
- data/lib/aws-sdk-resources/services/s3/encryptionV2/io_auth_decrypter.rb +58 -0
- data/lib/aws-sdk-resources/services/s3/encryptionV2/io_decrypter.rb +37 -0
- data/lib/aws-sdk-resources/services/s3/encryptionV2/io_encrypter.rb +73 -0
- data/lib/aws-sdk-resources/services/s3/encryptionV2/key_provider.rb +31 -0
- data/lib/aws-sdk-resources/services/s3/encryptionV2/kms_cipher_provider.rb +169 -0
- data/lib/aws-sdk-resources/services/s3/encryptionV2/materials.rb +60 -0
- data/lib/aws-sdk-resources/services/s3/encryptionV2/utils.rb +103 -0
- data/lib/aws-sdk-resources/services/s3/encryption_v2.rb +24 -0
- 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
|