File: decrypt_handler.rb

package info (click to toggle)
ruby-aws-sdk-s3 1.170.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 3,740 kB
  • sloc: ruby: 16,388; makefile: 3
file content (221 lines) | stat: -rw-r--r-- 7,544 bytes parent folder | download | duplicates (2)
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