Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 18 additions & 11 deletions .evergreen/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,10 @@ axes:
- id: "mongodb-version"
display_name: MongoDB Version
values:
- id: "latest"
display_name: "Latest"
variables:
MONGODB_VERSION: "latest"
- id: "rapid"
display_name: "Rapid"
variables:
Expand Down Expand Up @@ -1471,17 +1475,20 @@ buildvariants:
tasks:
- name: "test-fle"

# - matrix_name: "fle-latest"
# matrix_spec:
# auth-and-ssl: "noauth-and-nossl"
# ruby:
# topology: [replica-set, sharded-cluster]
# mongodb-version: [ 'latest' ]
# os: ubuntu2204
# fle: helper
# display_name: "FLE: ${mongodb-version} ${topology} ${ruby}"
# tasks:
# - name: "test-fle"
# Runs FLE specs against the latest (currently 9.0) server so that QE text
# query tests (which require server 9.0+) get real coverage on PRs.
- matrix_name: "fle-latest"
matrix_spec:
auth-and-ssl: "noauth-and-nossl"
ruby: "ruby-4.0"
topology: [replica-set, sharded-cluster]
mongodb-version: [ 'latest' ]
os: ubuntu2204
fle: helper
display_name: "FLE: ${mongodb-version} ${topology} ${ruby}"
tags: ["pr"]
tasks:
- name: "test-fle"

# Runs specs against the latest server. Not run on PRs to avoid blocking
# merges when an unreleased MongoDB build breaks something.
Expand Down
4 changes: 4 additions & 0 deletions .evergreen/config/axes.yml.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ axes:
- id: "mongodb-version"
display_name: MongoDB Version
values:
- id: "latest"
display_name: "Latest"
variables:
MONGODB_VERSION: "latest"
- id: "rapid"
display_name: "Rapid"
variables:
Expand Down
25 changes: 14 additions & 11 deletions .evergreen/config/standard.yml.erb
Original file line number Diff line number Diff line change
Expand Up @@ -364,17 +364,20 @@ buildvariants:
tasks:
- name: "test-fle"

# - matrix_name: "fle-latest"
# matrix_spec:
# auth-and-ssl: "noauth-and-nossl"
# ruby: <%#= latest_ruby %>
# topology: [replica-set, sharded-cluster]
# mongodb-version: [ 'latest' ]
# os: ubuntu2204
# fle: helper
# display_name: "FLE: ${mongodb-version} ${topology} ${ruby}"
# tasks:
# - name: "test-fle"
# Runs FLE specs against the latest (currently 9.0) server so that QE text
# query tests (which require server 9.0+) get real coverage on PRs.
- matrix_name: "fle-latest"
matrix_spec:
auth-and-ssl: "noauth-and-nossl"
ruby: <%= latest_ruby %>
topology: [replica-set, sharded-cluster]
mongodb-version: [ 'latest' ]
os: ubuntu2204
fle: helper
display_name: "FLE: ${mongodb-version} ${topology} ${ruby}"
tags: ["pr"]
tasks:
- name: "test-fle"

# Runs specs against the latest server. Not run on PRs to avoid blocking
# merges when an unreleased MongoDB build breaks something.
Expand Down
2 changes: 1 addition & 1 deletion gemfiles/standard.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,6 @@ def standard_dependencies
gem 'ruby-lsp', platforms: :mri
end

gem 'libmongocrypt-helper', '~> 1.19.0' if %w[helper mongocryptd].include?(ENV['FLE'])
gem 'libmongocrypt-helper', '~> 1.20.1' if %w[helper mongocryptd].include?(ENV['FLE'])
end
# rubocop:enable Metrics/MethodLength
21 changes: 14 additions & 7 deletions lib/mongo/client_encryption.rb
Original file line number Diff line number Diff line change
Expand Up @@ -100,15 +100,22 @@ def create_data_key(kms_provider, options = {})
# encryption key.
# @option options [ String ] :algorithm The algorithm used to encrypt the value.
# Valid algorithms are "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic",
# "AEAD_AES_256_CBC_HMAC_SHA_512-Random", "Indexed", "Unindexed".
# "AEAD_AES_256_CBC_HMAC_SHA_512-Random", "Indexed", "Unindexed", "String".
# @option options [ Integer | nil ] :contention_factor Contention factor
# to be applied if encryption algorithm is set to "Indexed". If not
# provided, it defaults to a value of 0. Contention factor should be set
# only if encryption algorithm is set to "Indexed".
# to be applied if encryption algorithm is set to "Indexed" or "String".
# If not provided, it defaults to a value of 0. Contention factor should be
# set only if encryption algorithm is set to "Indexed" or "String".
Comment on lines 104 to +107
# @option options [ String | nil ] query_type Query type to be applied
# if encryption algorithm is set to "Indexed". Query type should be set
# only if encryption algorithm is set to "Indexed". The only allowed
# value is "equality".
# if encryption algorithm is set to "Indexed" or "String". Allowed values
# are "equality" (for "Indexed") and "prefix", "suffix", "substring"
# (for "String").
Comment on lines 108 to +111
# @option options [ Hash | nil ] :string_opts Specifies index options for a
# Queryable Encryption field supporting "prefix", "suffix", or "substring"
# queries. Required when algorithm is "String". Allowed options are
# :case_sensitive, :diacritic_sensitive, :prefix, :suffix, :substring.
#
# @note The "substring" query type is unstable and subject to backwards
# breaking changes.
#
# @note The :key_id and :key_alt_name options are mutually exclusive. Only
# one is required to perform explicit encryption.
Expand Down
40 changes: 40 additions & 0 deletions lib/mongo/crypt/binding.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1873,6 +1873,46 @@ def self.ctx_setopt_algorithm_range(context, opts)
end
end

# @!method self.mongocrypt_ctx_setopt_algorithm_text(ctx, opts)
# @api private
#
# Set options for explicit encryption with the "String" algorithm.
#
# @note The libmongocrypt C function is named `..._algorithm_text` (the
# original "text" name), even though the algorithm string passed to
# mongocrypt_ctx_setopt_algorithm is "String" (renamed for GA). There is
# no `..._algorithm_string` symbol; `..._algorithm_text` is the correct
# and only setter for these options.
#
# @param [ FFI::Pointer ] ctx A pointer to a mongocrypt_ctx_t object.
# @param [ FFI::Pointer ] opts A pointer to a string options document.
#
# @return [ Boolean ] Whether setting this option succeeded.
attach_function(
:mongocrypt_ctx_setopt_algorithm_text,
%i[
pointer
pointer
],
:bool
)

# Set options for explicit encryption with the "String" algorithm.
#
# @param [ Mongo::Crypt::Context ] context
# @param [ Hash ] opts options
#
# @raise [ Mongo::Error::CryptError ] If the operation failed
def self.ctx_setopt_algorithm_text(context, opts)
validate_document(opts)
data = opts.to_bson.to_s
Binary.wrap_string(data) do |data_p|
check_ctx_status(context) do
mongocrypt_ctx_setopt_algorithm_text(context.ctx_p, data_p)
end
end
end

# Raise a Mongo::Error::CryptError based on the status of the underlying
# mongocrypt_t object.
#
Expand Down
61 changes: 49 additions & 12 deletions lib/mongo/crypt/explicit_encryption_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,16 @@ class ExplicitEncryptionContext < Context
# that will be used to encrypt the value.
# @option options [ String ] :algorithm The algorithm used to encrypt the
# value. Valid algorithms are "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic",
# "AEAD_AES_256_CBC_HMAC_SHA_512-Random", "Indexed", "Unindexed", "Range".
# "AEAD_AES_256_CBC_HMAC_SHA_512-Random", "Indexed", "Unindexed", "Range",
# "String".
# @option options [ Integer | nil ] :contention_factor Contention factor
# to be applied if encryption algorithm is set to "Indexed". If not
# provided, it defaults to a value of 0. Contention factor should be set
# only if encryption algorithm is set to "Indexed".
# to be applied if encryption algorithm is set to "Indexed", "Range", or "String".
# If not provided, it defaults to a value of 0. Contention factor should be set
# only if encryption algorithm is set to "Indexed", "Range", or "String".
# @option options [ String | nil ] query_type Query type to be applied
# if encryption algorithm is set to "Indexed" or "Range".
# Allowed values are "equality" and "range".
# if encryption algorithm is set to "Indexed", "Range", or "String".
# Allowed values are "equality", "range", "prefix", "suffix", and
# "substring".
# @option options [ Hash | nil ] :range_opts Specifies index options for
# a Queryable Encryption field supporting "range" queries.
# Allowed options are:
Expand All @@ -56,9 +58,22 @@ class ExplicitEncryptionContext < Context
# the encryptedFields of the destination collection.
# For double and decimal128, min/max/precision must all be set,
# or all be unset.
# @option options [ Hash | nil ] :string_opts Specifies index options for
# a Queryable Encryption field supporting "prefix", "suffix", or
# "substring" queries (algorithm "String"). Allowed options are:
# - :case_sensitive
# - :diacritic_sensitive
# - :prefix (Hash with :str_min_query_length, :str_max_query_length)
# - :suffix (Hash with :str_min_query_length, :str_max_query_length)
# - :substring (Hash with :str_max_length, :str_min_query_length,
# :str_max_query_length)
# The options must match the values set in the encryptedFields of the
# destination collection.
#
# @note The Range algorithm is experimental only. It is not intended for
# public use.
# @note The "substring" query type is unstable and subject to backwards
# breaking changes.
#
# @raise [ ArgumentError|Mongo::Error::CryptError ] If invalid options are provided
def initialize(mongocrypt, io, doc, options = {})
Expand Down Expand Up @@ -114,20 +129,22 @@ def set_key_alt_name(key_alt_name)

def set_algorithm_opts(options)
Binding.ctx_setopt_algorithm(self, options[:algorithm])
if %w[Indexed Range].include?(options[:algorithm])
if %w[Indexed Range String].include?(options[:algorithm])
Binding.ctx_setopt_contention_factor(self, options[:contention_factor]) if options[:contention_factor]
Binding.ctx_setopt_query_type(self, options[:query_type]) if options[:query_type]
else
if options[:contention_factor]
raise ArgumentError.new(':contention_factor is allowed only for "Indexed" or "Range" algorithms')
raise ArgumentError.new(':contention_factor is allowed only for "Indexed", "Range", or "String" algorithms')
end
if options[:query_type]
raise ArgumentError.new(':query_type is allowed only for "Indexed" or "Range" algorithms')
raise ArgumentError.new(':query_type is allowed only for "Indexed", "Range", or "String" algorithms')
end
end
return unless options[:algorithm] == 'Range'

Binding.ctx_setopt_algorithm_range(self, convert_range_opts(options[:range_opts]))
if options[:algorithm] == 'Range'
Binding.ctx_setopt_algorithm_range(self, convert_range_opts(options[:range_opts]))
elsif options[:algorithm] == 'String'
Binding.ctx_setopt_algorithm_text(self, convert_string_opts(options[:string_opts]))
end
end

def convert_range_opts(range_opts)
Expand All @@ -136,6 +153,26 @@ def convert_range_opts(range_opts)
opts[:trimFactor] = opts.delete(:trim_factor) if opts[:trim_factor]
end
end

def convert_string_opts(string_opts)
raise ArgumentError.new(':string_opts is required for the "String" algorithm') if string_opts.nil?

string_opts.dup.tap do |opts|
opts[:caseSensitive] = opts.delete(:case_sensitive) if opts.key?(:case_sensitive)
opts[:diacriticSensitive] = opts.delete(:diacritic_sensitive) if opts.key?(:diacritic_sensitive)
%i[substring prefix suffix].each do |query_type|
opts[query_type] = convert_string_query_opts(opts[query_type]) if opts[query_type]
end
end
end

def convert_string_query_opts(query_opts)
query_opts.dup.tap do |opts|
opts[:strMaxLength] = opts.delete(:str_max_length) if opts.key?(:str_max_length)
opts[:strMinQueryLength] = opts.delete(:str_min_query_length) if opts.key?(:str_min_query_length)
opts[:strMaxQueryLength] = opts.delete(:str_max_query_length) if opts.key?(:str_max_query_length)
end
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@
'explicit_encryption'
end

# Collection configured with contention 10, used by the non-zero contention
# case. Server 9.0+ requires the payload's contention to not exceed the
# collection's configured contention.
let(:encrypted_coll_c10) do
'explicit_encryption_c10'
end

let(:encrypted_fields_c10) do
BSON::ExtJSON.parse(File.read('spec/support/crypt/encrypted_fields/encryptedFields-c10.json'))
end

let(:value) do
'encrypted indexed value'
end
Expand Down Expand Up @@ -61,12 +72,15 @@
before do
authorized_client[encrypted_coll].drop(encrypted_fields: encrypted_fields)
authorized_client[encrypted_coll].create(encrypted_fields: encrypted_fields)
authorized_client[encrypted_coll_c10].drop(encrypted_fields: encrypted_fields_c10)
authorized_client[encrypted_coll_c10].create(encrypted_fields: encrypted_fields_c10)
authorized_client.use(key_vault_db)[key_vault_coll].drop
authorized_client.use(key_vault_db)[key_vault_coll, write_concern: { w: :majority }].insert_one(key1_document)
end

after do
authorized_client[encrypted_coll].drop(encrypted_fields: encrypted_fields)
authorized_client[encrypted_coll_c10].drop(encrypted_fields: encrypted_fields_c10)
authorized_client.use(key_vault_db)[key_vault_coll].drop
end

Expand All @@ -92,30 +106,20 @@
insert_payload = client_encryption.encrypt(
value, key_id: key1_id, algorithm: 'Indexed', contention_factor: 10
)
encrypted_client[encrypted_coll].insert_one(
encrypted_client[encrypted_coll_c10].insert_one(
'encryptedIndexed' => insert_payload
)
end
find_payload = client_encryption.encrypt(
value, key_id: key1_id, algorithm: 'Indexed', query_type: 'equality', contention_factor: 0
value, key_id: key1_id, algorithm: 'Indexed', query_type: 'equality', contention_factor: 10
)
find_results = encrypted_client[encrypted_coll]
find_results = encrypted_client[encrypted_coll_c10]
.find('encryptedIndexed' => find_payload)
.to_a
expect(find_results.size).to be < 10
expect(find_results.size).to eq(10)
find_results.each do |doc|
expect(doc['encryptedIndexed']).to eq(value)
end
find_payload2 = client_encryption.encrypt(
value, key_id: key1_id, algorithm: 'Indexed', query_type: 'equality', contention_factor: 10
)
find_results2 = encrypted_client[encrypted_coll]
.find('encryptedIndexed' => find_payload2)
.to_a
expect(find_results2.size).to eq(10)
find_results2.each do |doc|
expect(doc['encryptedIndexed']).to eq(value)
end
end

it 'can insert encrypted unindexed' do
Expand Down
Loading
Loading