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
|