class Mongo::Crypt::Handle

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

Attributes

kms_providers[R]

@returns [ Crypt::KMS::Credentials ] Credentials for KMS providers.

Public Class Methods

new(kms_providers, kms_tls_options, options={}) click to toggle source

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
# File lib/mongo/crypt/handle.rb, line 66
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)
  )

  @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

Public Instance Methods

crypt_shared_lib_available?() click to toggle source
# File lib/mongo/crypt/handle.rb, line 135
def crypt_shared_lib_available?
  crypt_shared_lib_version != 0
end
crypt_shared_lib_version() click to toggle source
# File lib/mongo/crypt/handle.rb, line 131
def crypt_shared_lib_version
  Binding.crypt_shared_lib_version(self)
end
kms_tls_options(provider) click to toggle source

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.

# File lib/mongo/crypt/handle.rb, line 127
def kms_tls_options(provider)
  @kms_tls_options.fetch(provider, {})
end
ref() click to toggle source

Return the reference to the underlying @mongocrypt object

@return [ FFI::Pointer ]

# File lib/mongo/crypt/handle.rb, line 117
def ref
  @mongocrypt
end

Private Instance Methods

do_aes(key_binary_p, iv_binary_p, input_binary_p, output_binary_p, response_length_p, status_p, decrypt: false, mode: :CBC) click to toggle source

Perform AES encryption or decryption and write the output to the provided mongocrypt_binary_t object.

# File lib/mongo/crypt/handle.rb, line 243
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
do_hmac_sha(digest_name, key_binary_p, input_binary_p, output_binary_p, status_p) click to toggle source

Perform HMAC SHA encryption and write the output to the provided mongocrypt_binary_t object.

# File lib/mongo/crypt/handle.rb, line 259
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
do_rsaes_pkcs_signature(key_binary_p, input_binary_p, output_binary_p, status_p) click to toggle source

Perform signing using RSASSA-PKCS1-v1_5 with SHA256 hash and write the output to the provided mongocrypt_binary_t object.

# File lib/mongo/crypt/handle.rb, line 271
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
handle_error(status_p) { || ... } click to toggle source

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.
# File lib/mongo/crypt/handle.rb, line 208
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
initialize_mongocrypt() click to toggle source

Initialize the underlying mongocrypt_t object and raise an error if the operation fails

# File lib/mongo/crypt/handle.rb, line 393
def initialize_mongocrypt
  Binding.init(self)
  # There is currently no test for the error(?) code path
end
maybe_set_schema_map(options) click to toggle source

Set the schema map option on the underlying mongocrypt_t object

# File lib/mongo/crypt/handle.rb, line 142
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
set_bypass_query_analysis() click to toggle source
# File lib/mongo/crypt/handle.rb, line 177
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
set_crypto_hooks() click to toggle source

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.

# File lib/mongo/crypt/handle.rb, line 288
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
set_encrypted_fields_map() click to toggle source
# File lib/mongo/crypt/handle.rb, line 167
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
set_logger_callback() click to toggle source

Send the logs from libmongocrypt to the Mongo::Logger

# File lib/mongo/crypt/handle.rb, line 188
def set_logger_callback
  @log_callback = Proc.new do |level, msg|
    @logger.send(level, msg)
  end

  Binding.setopt_log_handler(@mongocrypt, @log_callback)
end
write_binary_string_and_set_status(output_binary_p, status_p) { || ... } click to toggle source

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.
# File lib/mongo/crypt/handle.rb, line 233
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