1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
|
# frozen_string_literal: true
require 'base64'
module Aws
module S3
module Encryption
# @api private
class DecryptHandler < Seahorse::Client::Handler
@@warned_response_target_proc = false
V1_ENVELOPE_KEYS = %w(
x-amz-key
x-amz-iv
x-amz-matdesc
)
V2_ENVELOPE_KEYS = %w(
x-amz-key-v2
x-amz-iv
x-amz-cek-alg
x-amz-wrap-alg
x-amz-matdesc
)
V2_OPTIONAL_KEYS = %w(x-amz-tag-len)
POSSIBLE_ENVELOPE_KEYS = (V1_ENVELOPE_KEYS +
V2_ENVELOPE_KEYS + V2_OPTIONAL_KEYS).uniq
POSSIBLE_WRAPPING_FORMATS = %w(
AES/GCM
kms
kms+context
RSA-OAEP-SHA1
)
POSSIBLE_ENCRYPTION_FORMATS = %w(
AES/GCM/NoPadding
AES/CBC/PKCS5Padding
AES/CBC/PKCS7Padding
)
AUTH_REQUIRED_CEK_ALGS = %w(AES/GCM/NoPadding)
def call(context)
attach_http_event_listeners(context)
apply_cse_user_agent(context)
if context[:response_target].is_a?(Proc) && !@@warned_response_target_proc
@@warned_response_target_proc = true
warn(':response_target is a Proc, or a block was provided. ' \
'Read the entire object to the ' \
'end before you start using the decrypted data. This is to ' \
'verify that the object has not been modified since it ' \
'was encrypted.')
end
@handler.call(context)
end
private
def attach_http_event_listeners(context)
context.http_response.on_headers(200) do
cipher, envelope = decryption_cipher(context)
decrypter = body_contains_auth_tag?(envelope) ?
authenticated_decrypter(context, cipher, envelope) :
IODecrypter.new(cipher, context.http_response.body)
context.http_response.body = decrypter
end
context.http_response.on_success(200) do
decrypter = context.http_response.body
decrypter.finalize
decrypter.io.rewind if decrypter.io.respond_to?(:rewind)
context.http_response.body = decrypter.io
end
context.http_response.on_error do
if context.http_response.body.respond_to?(:io)
context.http_response.body = context.http_response.body.io
end
end
end
def decryption_cipher(context)
if (envelope = get_encryption_envelope(context))
cipher = context[:encryption][:cipher_provider]
.decryption_cipher(
envelope,
context[:encryption]
)
[cipher, envelope]
else
raise Errors::DecryptionError, "unable to locate encryption envelope"
end
end
def get_encryption_envelope(context)
if context[:encryption][:envelope_location] == :metadata
envelope_from_metadata(context) || envelope_from_instr_file(context)
else
envelope_from_instr_file(context) || envelope_from_metadata(context)
end
end
def envelope_from_metadata(context)
possible_envelope = {}
POSSIBLE_ENVELOPE_KEYS.each do |suffix|
if value = context.http_response.headers["x-amz-meta-#{suffix}"]
possible_envelope[suffix] = value
end
end
extract_envelope(possible_envelope)
end
def envelope_from_instr_file(context)
suffix = context[:encryption][:instruction_file_suffix]
possible_envelope = Json.load(context.client.get_object(
bucket: context.params[:bucket],
key: context.params[:key] + suffix
).body.read)
extract_envelope(possible_envelope)
rescue S3::Errors::ServiceError, Json::ParseError
nil
end
def extract_envelope(hash)
return nil unless hash
return v1_envelope(hash) if hash.key?('x-amz-key')
return v2_envelope(hash) if hash.key?('x-amz-key-v2')
if hash.keys.any? { |key| key.match(/^x-amz-key-(.+)$/) }
msg = "unsupported envelope encryption version #{$1}"
raise Errors::DecryptionError, msg
end
end
def v1_envelope(envelope)
envelope
end
def v2_envelope(envelope)
unless POSSIBLE_ENCRYPTION_FORMATS.include? envelope['x-amz-cek-alg']
alg = envelope['x-amz-cek-alg'].inspect
msg = "unsupported content encrypting key (cek) format: #{alg}"
raise Errors::DecryptionError, msg
end
unless POSSIBLE_WRAPPING_FORMATS.include? envelope['x-amz-wrap-alg']
alg = envelope['x-amz-wrap-alg'].inspect
msg = "unsupported key wrapping algorithm: #{alg}"
raise Errors::DecryptionError, msg
end
unless (missing_keys = V2_ENVELOPE_KEYS - envelope.keys).empty?
msg = "incomplete v2 encryption envelope:\n"
msg += " missing: #{missing_keys.join(',')}\n"
raise Errors::DecryptionError, msg
end
envelope
end
# This method fetches the tag from the end of the object by
# making a GET Object w/range request. This auth tag is used
# to initialize the cipher, and the decrypter truncates the
# auth tag from the body when writing the final bytes.
def authenticated_decrypter(context, cipher, envelope)
http_resp = context.http_response
content_length = http_resp.headers['content-length'].to_i
auth_tag_length = auth_tag_length(envelope)
auth_tag = context.client.get_object(
bucket: context.params[:bucket],
key: context.params[:key],
range: "bytes=-#{auth_tag_length}"
).body.read
cipher.auth_tag = auth_tag
cipher.auth_data = ''
# The encrypted object contains both the cipher text
# plus a trailing auth tag.
IOAuthDecrypter.new(
io: http_resp.body,
encrypted_content_length: content_length - auth_tag_length,
cipher: cipher)
end
def body_contains_auth_tag?(envelope)
AUTH_REQUIRED_CEK_ALGS.include?(envelope['x-amz-cek-alg'])
end
# Determine the auth tag length from the algorithm
# Validate it against the value provided in the x-amz-tag-len
# Return the tag length in bytes
def auth_tag_length(envelope)
tag_length =
case envelope['x-amz-cek-alg']
when 'AES/GCM/NoPadding' then AES_GCM_TAG_LEN_BYTES
else
raise ArgumentError, 'Unsupported cek-alg: ' \
"#{envelope['x-amz-cek-alg']}"
end
if (tag_length * 8) != envelope['x-amz-tag-len'].to_i
raise Errors::DecryptionError, 'x-amz-tag-len does not match expected'
end
tag_length
end
def apply_cse_user_agent(context)
if context.config.user_agent_suffix.nil?
context.config.user_agent_suffix = EC_USER_AGENT
elsif !context.config.user_agent_suffix.include? EC_USER_AGENT
context.config.user_agent_suffix += " #{EC_USER_AGENT}"
end
end
end
end
end
end
|