File: handle.rb

package info (click to toggle)
ruby-mongo 2.21.3-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 14,764 kB
  • sloc: ruby: 108,806; makefile: 5; sh: 2
file content (400 lines) | stat: -rw-r--r-- 14,970 bytes parent folder | download
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
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
# frozen_string_literal: true
# rubocop:todo all

# Copyright (C) 2019-2020 MongoDB Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

require 'ffi'
require 'base64'

module Mongo
  module Crypt

    # A handle to the libmongocrypt library that wraps a mongocrypt_t object,
    # allowing clients to set options on that object or perform operations such
    # as encryption and decryption
    #
    # @api private
    class Handle

      # @returns [ Crypt::KMS::Credentials ] Credentials for KMS providers.
      attr_reader :kms_providers

      # Creates a new Handle object and initializes it with options
      #
      # @param [ Crypt::KMS::Credentials ] kms_providers Credentials for KMS providers.
      #
      # @param [ Hash ] kms_tls_options TLS options to connect to KMS
      #   providers. Keys of the hash should be KSM provider names; values
      #   should be hashes of TLS connection options. The options are equivalent
      #   to TLS connection options of Mongo::Client.
      #
      # @param [ Hash ] options A hash of options.
      # @option options [ Hash | nil ] :schema_map A hash representing the JSON schema
      #   of the collection that stores auto encrypted documents. This option is
      #   mutually exclusive with :schema_map_path.
      # @option options [ String | nil ] :schema_map_path A path to a file contains the JSON schema
      #   of the collection that stores auto encrypted documents. This option is
      #   mutually exclusive with :schema_map.
      # @option options [ Hash | nil ] :encrypted_fields_map maps a collection
      #   namespace to an encryptedFields.
      #   - Note: If a collection is present on both the encryptedFieldsMap
      #     and schemaMap, an error will be raised.
      # @option options [ Boolean | nil ] :bypass_query_analysis When true
      #   disables automatic analysis of outgoing commands.
      # @option options [ String | nil ] :crypt_shared_lib_path Path that should
      #   be  the used to load the crypt shared library. Providing this option
      #   overrides default crypt shared library load paths for libmongocrypt.
      # @option options [ Boolean | nil ] :crypt_shared_lib_required Whether
      #   crypt_shared library is required. If 'true', an error will be raised
      #   if a crypt_shared library cannot be loaded by libmongocrypt.
      # @option options [ Boolean | nil ] :explicit_encryption_only Whether this
      #   handle is going to be used only for explicit encryption. If true,
      #   libmongocrypt is instructed not to load crypt shared library.
      # @option options [ Logger ] :logger A Logger object to which libmongocrypt logs
      #   will be sent
      def initialize(kms_providers, kms_tls_options, options={})
        # FFI::AutoPointer uses a custom release strategy to automatically free
        # the pointer once this object goes out of scope
        @mongocrypt = FFI::AutoPointer.new(
          Binding.mongocrypt_new,
          Binding.method(:mongocrypt_destroy)
        )
        Binding.kms_ctx_setopt_retry_kms(self, true)
        @kms_providers = kms_providers
        @kms_tls_options =  kms_tls_options

        maybe_set_schema_map(options)

        @encrypted_fields_map = options[:encrypted_fields_map]
        set_encrypted_fields_map if @encrypted_fields_map

        @bypass_query_analysis = options[:bypass_query_analysis]
        set_bypass_query_analysis if @bypass_query_analysis

        @crypt_shared_lib_path = options[:crypt_shared_lib_path]
        @explicit_encryption_only = options[:explicit_encryption_only]
        if @crypt_shared_lib_path
          Binding.setopt_set_crypt_shared_lib_path_override(self, @crypt_shared_lib_path)
        elsif !@bypass_query_analysis && !@explicit_encryption_only
          Binding.setopt_append_crypt_shared_lib_search_path(self, "$SYSTEM")
        end

        @logger = options[:logger]
        set_logger_callback if @logger

        set_crypto_hooks

        Binding.setopt_kms_providers(self, @kms_providers.to_document)

        if @kms_providers.aws&.empty? || @kms_providers.gcp&.empty? || @kms_providers.azure&.empty?
          Binding.setopt_use_need_kms_credentials_state(self)
        end

        initialize_mongocrypt

        @crypt_shared_lib_required = !!options[:crypt_shared_lib_required]
        if @crypt_shared_lib_required && crypt_shared_lib_version == 0
          raise Mongo::Error::CryptError.new(
            "Crypt shared library is required, but cannot be loaded  according to libmongocrypt"
          )
        end
      end

      # Return the reference to the underlying @mongocrypt object
      #
      # @return [ FFI::Pointer ]
      def ref
        @mongocrypt
      end

      # Return TLS options for KMS provider. If there are no TLS options set,
      # empty hash is returned.
      #
      # @param [ String ] provider KSM provider name.
      #
      # @return [ Hash ] TLS options to connect to KMS provider.
      def kms_tls_options(provider)
        @kms_tls_options.fetch(provider, {})
      end

      def crypt_shared_lib_version
        Binding.crypt_shared_lib_version(self)
      end

      def crypt_shared_lib_available?
        crypt_shared_lib_version != 0
      end

      private

      # Set the schema map option on the underlying mongocrypt_t object
      def maybe_set_schema_map(options)
        if !options[:schema_map] && !options[:schema_map_path]
          @schema_map = nil
        elsif options[:schema_map] && options[:schema_map_path]
          raise ArgumentError.new(
            "Cannot set both schema_map and schema_map_path options."
          )
        elsif options[:schema_map]
          unless options[:schema_map].is_a?(Hash)
            raise ArgumentError.new(
              "#{@schema_map} is an invalid schema_map; schema_map must be a Hash or nil."
            )
          end
          @schema_map = options[:schema_map]
          Binding.setopt_schema_map(self, @schema_map)
        elsif options[:schema_map_path]
          @schema_map = BSON::ExtJSON.parse(File.read(options[:schema_map_path]))
          Binding.setopt_schema_map(self, @schema_map)
        end
      rescue Errno::ENOENT
        raise ArgumentError.new(
          "#{@schema_map_path} is an invalid path to a file contains schema_map."
        )
      end

      def set_encrypted_fields_map
        unless @encrypted_fields_map.is_a?(Hash)
          raise ArgumentError.new(
            "#{@encrypted_fields_map} is an invalid encrypted_fields_map: must be a Hash or nil"
          )
        end

        Binding.setopt_encrypted_field_config_map(self, @encrypted_fields_map)
      end

      def set_bypass_query_analysis
        unless [true, false].include?(@bypass_query_analysis)
          raise ArgumentError.new(
            "#{@bypass_query_analysis} is an invalid bypass_query_analysis value; must be a Boolean or nil"
          )
        end

        Binding.setopt_bypass_query_analysis(self) if @bypass_query_analysis
      end

      # Send the logs from libmongocrypt to the Mongo::Logger
      def set_logger_callback
        @log_callback = Proc.new do |level, msg|
          @logger.send(level, msg)
        end

        Binding.setopt_log_handler(@mongocrypt, @log_callback)
      end

      # Yields to the provided block and rescues exceptions raised by
      # the block. If an exception was raised, sets the specified status
      # to the exception message and returns false. If no exceptions were
      # raised, does not modify the status and returns true.
      #
      # This method is meant to be used with libmongocrypt callbacks and
      # follows the API defined by libmongocrypt.
      #
      # @param [ FFI::Pointer ] status_p A pointer to libmongocrypt status object
      #
      # @return [ true | false ] Whether block executed without raising
      #   exceptions.
      def handle_error(status_p)
        begin
          yield

          true
        rescue => e
          status = Status.from_pointer(status_p)
          status.update(:error_client, 1, "#{e.class}: #{e}")
          false
        end
      end

      # Yields to the provided block and writes the return value of block
      # to the specified mongocrypt_binary_t object. If an exception is
      # raised during execution of the block, writes the exception message
      # to the specified status object and returns false. If no exception is
      # raised, does not modify status and returns true.
      # message to the mongocrypt_status_t object.
      #
      # @param [ FFI::Pointer ] output_binary_p A pointer to libmongocrypt
      #   Binary object to receive the result of block's execution
      # @param [ FFI::Pointer ] status_p A pointer to libmongocrypt status object
      #
      # @return [ true | false ] Whether block executed without raising
      #   exceptions.
      def write_binary_string_and_set_status(output_binary_p, status_p)
        handle_error(status_p) do
          output = yield

          Binary.from_pointer(output_binary_p).write(output)
        end
      end

      # Perform AES encryption or decryption and write the output to the
      # provided mongocrypt_binary_t object.
      def do_aes(key_binary_p, iv_binary_p, input_binary_p, output_binary_p,
        response_length_p, status_p, decrypt: false, mode: :CBC)
        key = Binary.from_pointer(key_binary_p).to_s
        iv = Binary.from_pointer(iv_binary_p).to_s
        input = Binary.from_pointer(input_binary_p).to_s

        write_binary_string_and_set_status(output_binary_p, status_p) do
          output = Hooks.aes(key, iv, input, decrypt: decrypt, mode: mode)
          response_length_p.write_int(output.bytesize)

          output
        end
      end

      # Perform HMAC SHA encryption and write the output to the provided
      # mongocrypt_binary_t object.
      def do_hmac_sha(digest_name, key_binary_p, input_binary_p,
        output_binary_p, status_p)
        key = Binary.from_pointer(key_binary_p).to_s
        input = Binary.from_pointer(input_binary_p).to_s

        write_binary_string_and_set_status(output_binary_p, status_p) do
          Hooks.hmac_sha(digest_name, key, input)
        end
      end

      # Perform signing using RSASSA-PKCS1-v1_5 with SHA256 hash and write
      # the output to the provided mongocrypt_binary_t object.
      def do_rsaes_pkcs_signature(key_binary_p, input_binary_p,
        output_binary_p, status_p)
        key = Binary.from_pointer(key_binary_p).to_s
        input = Binary.from_pointer(input_binary_p).to_s

        write_binary_string_and_set_status(output_binary_p, status_p) do
          Hooks.rsaes_pkcs_signature(key, input)
        end
      end

      # We are building libmongocrypt without crypto functions to remove the
      # external dependency on OpenSSL. This method binds native Ruby crypto
      # methods to the underlying mongocrypt_t object so that libmongocrypt can
      # still perform cryptography.
      #
      # Every crypto binding ignores its first argument, which is an option
      # mongocrypt_ctx_t object and is not required to use crypto hooks.
      def set_crypto_hooks
        @aes_encrypt = Proc.new do |_, key_binary_p, iv_binary_p, input_binary_p,
          output_binary_p, response_length_p, status_p|
          do_aes(
            key_binary_p,
            iv_binary_p,
            input_binary_p,
            output_binary_p,
            response_length_p,
            status_p
          )
        end

        @aes_decrypt = Proc.new do |_, key_binary_p, iv_binary_p, input_binary_p,
          output_binary_p, response_length_p, status_p|
          do_aes(
            key_binary_p,
            iv_binary_p,
            input_binary_p,
            output_binary_p,
            response_length_p,
            status_p,
            decrypt: true
          )
        end

        @random = Proc.new do |_, output_binary_p, num_bytes, status_p|
          write_binary_string_and_set_status(output_binary_p, status_p) do
            Hooks.random(num_bytes)
          end
        end

        @hmac_sha_512 = Proc.new do |_, key_binary_p, input_binary_p,
          output_binary_p, status_p|
          do_hmac_sha('SHA512', key_binary_p, input_binary_p, output_binary_p, status_p)
        end

        @hmac_sha_256 = Proc.new do |_, key_binary_p, input_binary_p,
          output_binary_p, status_p|
          do_hmac_sha('SHA256', key_binary_p, input_binary_p, output_binary_p, status_p)
        end

        @hmac_hash = Proc.new do |_, input_binary_p, output_binary_p, status_p|
          input = Binary.from_pointer(input_binary_p).to_s

          write_binary_string_and_set_status(output_binary_p, status_p) do
            Hooks.hash_sha256(input)
          end
        end

        Binding.setopt_crypto_hooks(
          self,
          @aes_encrypt,
          @aes_decrypt,
          @random,
          @hmac_sha_512,
          @hmac_sha_256,
          @hmac_hash,
        )

        @aes_ctr_encrypt = Proc.new do |_, key_binary_p, iv_binary_p, input_binary_p,
          output_binary_p, response_length_p, status_p|
          do_aes(
            key_binary_p,
            iv_binary_p,
            input_binary_p,
            output_binary_p,
            response_length_p,
            status_p,
            mode: :CTR,
          )
        end

        @aes_ctr_decrypt = Proc.new do |_, key_binary_p, iv_binary_p, input_binary_p,
          output_binary_p, response_length_p, status_p|
          do_aes(
            key_binary_p,
            iv_binary_p,
            input_binary_p,
            output_binary_p,
            response_length_p,
            status_p,
            decrypt: true,
            mode: :CTR,
          )
        end

        Binding.setopt_aes_256_ctr(
          self,
          @aes_ctr_encrypt,
          @aes_ctr_decrypt,
        )

        @rsaes_pkcs_signature_cb = Proc.new do |_, key_binary_p, input_binary_p,
          output_binary_p, status_p|
          do_rsaes_pkcs_signature(key_binary_p, input_binary_p, output_binary_p, status_p)
        end

        Binding.setopt_crypto_hook_sign_rsaes_pkcs1_v1_5(
          self,
          @rsaes_pkcs_signature_cb
        )
      end

      # Initialize the underlying mongocrypt_t object and raise an error if the operation fails
      def initialize_mongocrypt
        Binding.init(self)
        # There is currently no test for the error(?) code path
      end
    end
  end
end