Skip to content

Commit 699f588

Browse files
committed
Updates to the Amazon S3 Encryption Client
This change includes fixes for issues that were reported by Sophie Schmieg from the Google ISE team, and for issues that were discovered by AWS Cryptography.
1 parent c99d692 commit 699f588

37 files changed

+3731
-267
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
Unreleased Changes
22
------------------
33

4+
* Feature - Updates to the Amazon S3 Encryption Client. This change includes fixes for issues that were reported by Sophie Schmieg from the Google ISE team, and for issues that were discovered by AWS Cryptography.
5+
46
2.11.561 (2020-08-06)
57
------------------
68

aws-sdk-resources/lib/aws-sdk-resources/services/s3.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ module S3
77
require 'aws-sdk-resources/services/s3/multipart_upload'
88

99
autoload :Encryption, 'aws-sdk-resources/services/s3/encryption'
10+
autoload :EncryptionV2, 'aws-sdk-resources/services/s3/encryption_v2'
1011
autoload :FilePart, 'aws-sdk-resources/services/s3/file_part'
1112
autoload :FileUploader, 'aws-sdk-resources/services/s3/file_uploader'
1213
autoload :FileDownloader, 'aws-sdk-resources/services/s3/file_downloader'

aws-sdk-resources/lib/aws-sdk-resources/services/s3/encryption.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ module Aws
22
module S3
33
module Encryption
44

5+
AES_GCM_TAG_LEN_BYTES = 16
6+
EC_USER_AGENT = 'S3CryptoV1n'
7+
58
autoload :Client, 'aws-sdk-resources/services/s3/encryption/client'
69
autoload :DecryptHandler, 'aws-sdk-resources/services/s3/encryption/decrypt_handler'
710
autoload :DefaultCipherProvider, 'aws-sdk-resources/services/s3/encryption/default_cipher_provider'

aws-sdk-resources/lib/aws-sdk-resources/services/s3/encryption/client.rb

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1+
# frozen_string_literal: true
2+
3+
require 'forwardable'
4+
15
module Aws
26
module S3
37

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}.
412
# Provides an encryption client that encrypts and decrypts data client-side,
513
# storing the encrypted data in Amazon S3.
614
#
@@ -26,7 +34,7 @@ module S3
2634
# data client-side.
2735
#
2836
# One of the benefits of envelope encryption is that if your master key
29-
# is compromised, you have the option of jut re-encrypting the stored
37+
# is compromised, you have the option of just re-encrypting the stored
3038
# envelope symmetric keys, instead of re-encrypting all of the
3139
# data in your account.
3240
#
@@ -178,15 +186,17 @@ module Encryption
178186
class Client
179187

180188
extend Deprecations
189+
extend Forwardable
190+
def_delegators :@client, :config, :delete_object, :head_object, :build_request
181191

182-
# Creates a new encryption client. You must provide on of the following
192+
# Creates a new encryption client. You must provide one of the following
183193
# options:
184194
#
185195
# * `:encryption_key`
186196
# * `:kms_key_id`
187197
# * `:key_provider`
188198
#
189-
# You may also pass any other options accepted by {S3::Client#initialize}.
199+
# You may also pass any other options accepted by `Client#initialize`.
190200
#
191201
# @option options [S3::Client] :client A basic S3 client that is used
192202
# to make api calls. If a `:client` is not provided, a new {S3::Client}
@@ -223,6 +233,13 @@ def initialize(options = {})
223233
@envelope_location = extract_location(options)
224234
@instruction_file_suffix = extract_suffix(options)
225235
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+
226243

227244
# @return [S3::Client]
228245
attr_reader :client
@@ -327,7 +344,7 @@ def extract_key_provider(options)
327344
elsif options[:encryption_key]
328345
DefaultKeyProvider.new(options)
329346
else
330-
msg = "you must pass a :kms_key_id, :key_provider, or :encryption_key"
347+
msg = 'you must pass a :kms_key_id, :key_provider, or :encryption_key'
331348
raise ArgumentError, msg
332349
end
333350
end
@@ -347,8 +364,8 @@ def extract_location(options)
347364
if [:metadata, :instruction_file].include?(location)
348365
location
349366
else
350-
msg = ":envelope_location must be :metadata or :instruction_file "
351-
msg << "got #{location.inspect}"
367+
msg = ':envelope_location must be :metadata or :instruction_file '\
368+
"got #{location.inspect}"
352369
raise ArgumentError, msg
353370
end
354371
end
@@ -358,7 +375,7 @@ def extract_suffix(options)
358375
if String === suffix
359376
suffix
360377
else
361-
msg = ":instruction_file_suffix must be a String"
378+
msg = ':instruction_file_suffix must be a String'
362379
raise ArgumentError, msg
363380
end
364381
end

aws-sdk-resources/lib/aws-sdk-resources/services/s3/encryption/decrypt_handler.rb

Lines changed: 65 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# frozen_string_literal: true
2+
13
require 'base64'
24

35
module Aws
@@ -20,15 +22,29 @@ class DecryptHandler < Seahorse::Client::Handler
2022
x-amz-matdesc
2123
)
2224

23-
POSSIBLE_ENVELOPE_KEYS = (V1_ENVELOPE_KEYS + V2_ENVELOPE_KEYS).uniq
25+
V2_OPTIONAL_KEYS = %w(x-amz-tag-len)
26+
27+
POSSIBLE_ENVELOPE_KEYS = (V1_ENVELOPE_KEYS +
28+
V2_ENVELOPE_KEYS + V2_OPTIONAL_KEYS).uniq
29+
30+
POSSIBLE_WRAPPING_FORMATS = %w(
31+
AES/GCM
32+
kms
33+
kms+context
34+
RSA-OAEP-SHA1
35+
)
2436

2537
POSSIBLE_ENCRYPTION_FORMATS = %w(
2638
AES/GCM/NoPadding
2739
AES/CBC/PKCS5Padding
40+
AES/CBC/PKCS7Padding
2841
)
2942

43+
AUTH_REQUIRED_CEK_ALGS = %w(AES/GCM/NoPadding)
44+
3045
def call(context)
3146
attach_http_event_listeners(context)
47+
apply_cse_user_agent(context)
3248
@handler.call(context)
3349
end
3450

@@ -37,9 +53,9 @@ def call(context)
3753
def attach_http_event_listeners(context)
3854

3955
context.http_response.on_headers(200) do
40-
cipher = decryption_cipher(context)
41-
decrypter = body_contains_auth_tag?(context) ?
42-
authenticated_decrypter(context, cipher) :
56+
cipher, envelope = decryption_cipher(context)
57+
decrypter = body_contains_auth_tag?(envelope) ?
58+
authenticated_decrypter(context, cipher, envelope) :
4359
IODecrypter.new(cipher, context.http_response.body)
4460
context.http_response.body = decrypter
4561
end
@@ -60,7 +76,12 @@ def attach_http_event_listeners(context)
6076

6177
def decryption_cipher(context)
6278
if envelope = get_encryption_envelope(context)
63-
context[:encryption][:cipher_provider].decryption_cipher(envelope)
79+
cipher = context[:encryption][:cipher_provider]
80+
.decryption_cipher(
81+
envelope,
82+
kms_encryption_context: context[:encryption][:kms_encryption_context]
83+
)
84+
[cipher, envelope]
6485
else
6586
raise Errors::DecryptionError, "unable to locate encryption envelope"
6687
end
@@ -96,13 +117,12 @@ def envelope_from_instr_file(context)
96117
end
97118

98119
def extract_envelope(hash)
120+
return nil unless hash
99121
return v1_envelope(hash) if hash.key?('x-amz-key')
100122
return v2_envelope(hash) if hash.key?('x-amz-key-v2')
101123
if hash.keys.any? { |key| key.match(/^x-amz-key-(.+)$/) }
102124
msg = "unsupported envelope encryption version #{$1}"
103125
raise Errors::DecryptionError, msg
104-
else
105-
nil # no envelope found
106126
end
107127
end
108128

@@ -116,35 +136,31 @@ def v2_envelope(envelope)
116136
msg = "unsupported content encrypting key (cek) format: #{alg}"
117137
raise Errors::DecryptionError, msg
118138
end
119-
unless envelope['x-amz-wrap-alg'] == 'kms'
120-
# possible to support
121-
# RSA/ECB/OAEPWithSHA-256AndMGF1Padding
139+
unless POSSIBLE_WRAPPING_FORMATS.include? envelope['x-amz-wrap-alg']
122140
alg = envelope['x-amz-wrap-alg'].inspect
123141
msg = "unsupported key wrapping algorithm: #{alg}"
124142
raise Errors::DecryptionError, msg
125143
end
126-
unless V2_ENVELOPE_KEYS.sort == envelope.keys.sort
144+
unless (missing_keys = V2_ENVELOPE_KEYS - envelope.keys).empty?
127145
msg = "incomplete v2 encryption envelope:\n"
128-
msg += " expected: #{V2_ENVELOPE_KEYS.join(',')}\n"
129-
msg += " got: #{envelope_keys.join(', ')}"
146+
msg += " missing: #{missing_keys.join(',')}\n"
130147
raise Errors::DecryptionError, msg
131148
end
132149
envelope
133150
end
134151

135-
# When the x-amz-meta-x-amz-tag-len header is present, it indicates
136-
# that the body of this object has a trailing auth tag. The header
137-
# indicates the length of that tag.
138-
#
139152
# This method fetches the tag from the end of the object by
140153
# making a GET Object w/range request. This auth tag is used
141154
# to initialize the cipher, and the decrypter truncates the
142155
# auth tag from the body when writing the final bytes.
143-
def authenticated_decrypter(context, cipher)
156+
def authenticated_decrypter(context, cipher, envelope)
157+
if RUBY_VERSION.match(/1.9/)
158+
raise "authenticated decryption not supported by OpenSSL in Ruby version ~> 1.9"
159+
raise Aws::Errors::NonSupportedRubyVersionError, msg
160+
end
144161
http_resp = context.http_response
145162
content_length = http_resp.headers['content-length'].to_i
146-
auth_tag_length = http_resp.headers['x-amz-meta-x-amz-tag-len']
147-
auth_tag_length = auth_tag_length.to_i / 8
163+
auth_tag_length = auth_tag_length(envelope)
148164

149165
auth_tag = context.client.get_object(
150166
bucket: context.params[:bucket],
@@ -156,16 +172,40 @@ def authenticated_decrypter(context, cipher)
156172
cipher.auth_data = ''
157173

158174
# The encrypted object contains both the cipher text
159-
# plus a trailing auth tag. This decrypter will the body
160-
# expect for the trailing auth tag.
161-
decrypter = IOAuthDecrypter.new(
175+
# plus a trailing auth tag.
176+
IOAuthDecrypter.new(
162177
io: http_resp.body,
163178
encrypted_content_length: content_length - auth_tag_length,
164179
cipher: cipher)
165180
end
166181

167-
def body_contains_auth_tag?(context)
168-
context.http_response.headers['x-amz-meta-x-amz-tag-len']
182+
def body_contains_auth_tag?(envelope)
183+
AUTH_REQUIRED_CEK_ALGS.include?(envelope['x-amz-cek-alg'])
184+
end
185+
186+
# Determine the auth tag length from the algorithm
187+
# Validate it against the value provided in the x-amz-tag-len
188+
# Return the tag length in bytes
189+
def auth_tag_length(envelope)
190+
tag_length =
191+
case envelope['x-amz-cek-alg']
192+
when 'AES/GCM/NoPadding' then AES_GCM_TAG_LEN_BYTES
193+
else
194+
raise ArgumentError, 'Unsupported cek-alg: ' \
195+
"#{envelope['x-amz-cek-alg']}"
196+
end
197+
if (tag_length * 8) != envelope['x-amz-tag-len'].to_i
198+
raise Errors::DecryptionError, 'x-amz-tag-len does not match expected'
199+
end
200+
tag_length
201+
end
202+
203+
def apply_cse_user_agent(context)
204+
if context.config.user_agent_suffix.nil?
205+
context.config.user_agent_suffix = EC_USER_AGENT
206+
elsif !context.config.user_agent_suffix.include? EC_USER_AGENT
207+
context.config.user_agent_suffix += " #{EC_USER_AGENT}"
208+
end
169209
end
170210

171211
end

aws-sdk-resources/lib/aws-sdk-resources/services/s3/encryption/default_cipher_provider.rb

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# frozen_string_literal: true
2+
13
require 'base64'
24

35
module Aws
@@ -24,11 +26,48 @@ def encryption_cipher
2426

2527
# @return [Cipher] Given an encryption envelope, returns a
2628
# decryption cipher.
27-
def decryption_cipher(envelope)
29+
def decryption_cipher(envelope, options = {})
2830
master_key = @key_provider.key_for(envelope['x-amz-matdesc'])
29-
key = Utils.decrypt(master_key, decode64(envelope['x-amz-key']))
30-
iv = decode64(envelope['x-amz-iv'])
31-
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
3271
end
3372

3473
private
@@ -56,7 +95,6 @@ def encode64(str)
5695
def decode64(str)
5796
Base64.decode64(str)
5897
end
59-
6098
end
6199
end
62100
end

aws-sdk-resources/lib/aws-sdk-resources/services/s3/encryption/default_key_provider.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# frozen_string_literal: true
2+
13
module Aws
24
module S3
35
module Encryption

aws-sdk-resources/lib/aws-sdk-resources/services/s3/encryption/encrypt_handler.rb

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# frozen_string_literal: true
2+
13
require 'base64'
24

35
module Aws
@@ -10,6 +12,7 @@ def call(context)
1012
envelope, cipher = context[:encryption][:cipher_provider].encryption_cipher
1113
apply_encryption_envelope(context, envelope, cipher)
1214
apply_encryption_cipher(context, cipher)
15+
apply_cse_user_agent(context)
1316
@handler.call(context)
1417
end
1518

@@ -36,14 +39,22 @@ def apply_encryption_cipher(context, cipher)
3639
context.params[:body] = IOEncrypter.new(cipher, io)
3740
context.params[:metadata] ||= {}
3841
context.params[:metadata]['x-amz-unencrypted-content-length'] = io.size
39-
if md5 = context.params.delete(:content_md5)
40-
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')
4144
end
4245
context.http_response.on_headers do
4346
context.params[:body].close
4447
end
4548
end
4649

50+
def apply_cse_user_agent(context)
51+
if context.config.user_agent_suffix.nil?
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+
end
56+
end
57+
4758
end
4859
end
4960
end

0 commit comments

Comments
 (0)