Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions lib/mongo/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,10 @@ def hash
# - :crypt_shared_lib_required => [ Boolean | nil ] Whether
# crypt shared library is required. If 'true', an error will be raised
# if a crypt_shared library cannot be loaded by libmongocrypt.
# - :key_expiration_ms => Integer | nil, the lifetime of the data
# encryption key cache, in milliseconds. Must be a non-negative
# integer. A value of 0 means the cache never expires. Defaults to
# 60000.
#
# Notes on automatic encryption:
# - Automatic encryption is an enterprise only feature that only applies
Expand Down
7 changes: 6 additions & 1 deletion lib/mongo/client_encryption.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ class ClientEncryption
# @option options [ Integer ] :timeout_ms The operation timeout in milliseconds.
# Must be a non-negative integer. An explicit value of 0 means infinite.
# The default value is unset which means the feature is disabled.
# @option options [ Integer ] :key_expiration_ms The lifetime of the data
# encryption key cache, in milliseconds. Must be a non-negative integer.
# An explicit value of 0 means the cache never expires. The default is
# 60000.
Comment on lines +45 to +48
#
# @raise [ ArgumentError ] If required options are missing or incorrectly
# formatted.
Expand All @@ -51,7 +55,8 @@ def initialize(key_vault_client, options = {})
options[:key_vault_namespace],
Crypt::KMS::Credentials.new(options[:kms_providers]),
Crypt::KMS::Validations.validate_tls_options(options[:kms_tls_options]),
options[:timeout_ms]
options[:timeout_ms],
options[:key_expiration_ms]
)
end

Expand Down
6 changes: 5 additions & 1 deletion lib/mongo/crypt/auto_encrypter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ class AutoEncrypter
# @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 [ Integer | nil ] :key_expiration_ms The lifetime of the
# data encryption key cache, in milliseconds. A value of 0 means the
# cache never expires. Defaults to 60000.
#
# @raise [ ArgumentError ] If required options are missing or incorrectly
# formatted.
Expand All @@ -99,7 +102,8 @@ def initialize(options)
bypass_query_analysis: @options[:bypass_query_analysis],
crypt_shared_lib_path: @options[:extra_options][:crypt_shared_lib_path],
crypt_shared_lib_required: @options[:extra_options][:crypt_shared_lib_required],
disable_crypt_shared_lib_search: @options[:extra_options][:disable_crypt_shared_lib_search]
disable_crypt_shared_lib_search: @options[:extra_options][:disable_crypt_shared_lib_search],
key_expiration_ms: @options[:key_expiration_ms]
)

@mongocryptd_options = @options[:extra_options].slice(
Expand Down
25 changes: 25 additions & 0 deletions lib/mongo/crypt/binding.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1479,6 +1479,31 @@ def self.setopt_bypass_query_analysis(handle)
mongocrypt_setopt_bypass_query_analysis(handle.ref)
end

# @!method self.mongocrypt_setopt_key_expiration(crypt, cache_expiration_ms)
# @api private
#
# Set the expiration time for the data encryption key cache.
#
# @param [ FFI::Pointer ] crypt A pointer to a mongocrypt_t object.
# @param [ Integer ] cache_expiration_ms The cache expiration time in
# milliseconds. If zero, the cache never expires.
# @return [ Boolean ] Returns whether the option was set successfully.
attach_function(:mongocrypt_setopt_key_expiration, %i[pointer uint64], :bool)

# Set the expiration time for the data encryption key cache on the
# Mongo::Crypt::Handle object.
#
# @param [ Mongo::Crypt::Handle ] handle
# @param [ Integer ] cache_expiration_ms The cache expiration time in
# milliseconds. If zero, the cache never expires.
#
# @raise [ Mongo::Error::CryptError ] If the option is not set successfully.
def self.setopt_key_expiration(handle, cache_expiration_ms)
check_status(handle) do
mongocrypt_setopt_key_expiration(handle.ref, cache_expiration_ms)
end
end

# @!method self.mongocrypt_setopt_aes_256_ctr(crypt, aes_256_ctr_encrypt, aes_256_ctr_decrypt, ctx)
# @api private
#
Expand Down
11 changes: 9 additions & 2 deletions lib/mongo/crypt/explicit_encrypter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,19 @@ class ExplicitEncrypter
# to TLS connection options of Mongo::Client.
# @param [ Integer | nil ] timeout_ms Timeout for every operation executed
# on this object.
def initialize(key_vault_client, key_vault_namespace, kms_providers, kms_tls_options, timeout_ms = nil)
# @param [ Integer | nil ] key_expiration_ms The lifetime of the data
# encryption key cache, in milliseconds. A value of 0 means the cache
# never expires. When nil, libmongocrypt's default of 60000 is used.
def initialize(
key_vault_client, key_vault_namespace, kms_providers, kms_tls_options,
timeout_ms = nil, key_expiration_ms = nil
)
Crypt.validate_ffi!
@crypt_handle = Handle.new(
kms_providers,
kms_tls_options,
explicit_encryption_only: true
explicit_encryption_only: true,
key_expiration_ms: key_expiration_ms
)
@encryption_io = EncryptionIO.new(
key_vault_client: key_vault_client,
Expand Down
18 changes: 18 additions & 0 deletions lib/mongo/crypt/handle.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ class Handle
# error that libmongocrypt raises on a subsequent "$SYSTEM" search.
# @option options [ Logger ] :logger A Logger object to which libmongocrypt logs
# will be sent
# @option options [ Integer | nil ] :key_expiration_ms The lifetime of the
# data encryption key cache, in milliseconds. A value of 0 means the
# cache never expires. When nil, libmongocrypt's default of 60000 is
# used.
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
Expand All @@ -87,6 +91,9 @@ def initialize(kms_providers, kms_tls_options, options = {})
@bypass_query_analysis = options[:bypass_query_analysis]
set_bypass_query_analysis if @bypass_query_analysis

@key_expiration_ms = options[:key_expiration_ms]
set_key_expiration unless @key_expiration_ms.nil?

@crypt_shared_lib_path = options[:crypt_shared_lib_path]
@explicit_encryption_only = options[:explicit_encryption_only]
@disable_crypt_shared_lib_search = options[:disable_crypt_shared_lib_search]
Expand Down Expand Up @@ -200,6 +207,17 @@ def set_bypass_query_analysis
Binding.setopt_bypass_query_analysis(self) if @bypass_query_analysis
end

def set_key_expiration
unless @key_expiration_ms.is_a?(Integer) && !@key_expiration_ms.negative?
raise ArgumentError.new(
"#{@key_expiration_ms} is an invalid key_expiration_ms value; " \
'must be a non-negative Integer or nil'
)
end
Comment on lines +210 to +216

Binding.setopt_key_expiration(self, @key_expiration_ms)
end

# Send the logs from libmongocrypt to the Mongo::Logger
def set_logger_callback
@log_callback = proc do |level, msg|
Expand Down
53 changes: 52 additions & 1 deletion spec/mongo/crypt/handle_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
bypass_query_analysis: bypass_query_analysis,
crypt_shared_lib_path: crypt_shared_lib_path,
crypt_shared_lib_required: crypt_shared_lib_required,
explicit_encryption_only: explicit_encryption_only
explicit_encryption_only: explicit_encryption_only,
key_expiration_ms: key_expiration_ms
)
end

Expand Down Expand Up @@ -48,6 +49,10 @@
nil
end

let(:key_expiration_ms) do
nil
end

shared_examples 'a functioning Mongo::Crypt::Handle' do
context 'with valid schema map' do
it 'does not raise an exception' do
Expand Down Expand Up @@ -219,6 +224,52 @@
end
end

# The happy path (a short expiration causes the DEK to be re-fetched) is
# covered by the keyCache.yml unified spec test. These specs cover the
# validation and pass-through behavior that YAML cannot express.
context 'key_expiration_ms' do
include_context 'with local kms_providers'

context 'when not given' do
it 'leaves the libmongocrypt default in place' do
expect(Mongo::Crypt::Binding).not_to receive(:setopt_key_expiration)

handle
end
end

context 'when zero' do
let(:key_expiration_ms) { 0 }

it 'passes it through to mean "never expire"' do
expect(Mongo::Crypt::Binding).to receive(:setopt_key_expiration)
.with(anything, 0).and_call_original

handle
end
end

context 'when negative' do
let(:key_expiration_ms) { -1 }

it 'raises an exception' do
expect { handle }.to raise_error(
ArgumentError, /invalid key_expiration_ms value; must be a non-negative Integer or nil/
)
end
end

context 'when not an Integer' do
let(:key_expiration_ms) { '60000' }

it 'raises an exception' do
expect { handle }.to raise_error(
ArgumentError, /invalid key_expiration_ms value; must be a non-negative Integer or nil/
)
end
end
end

context 'AWS' do
context 'with valid AWS kms_providers' do
include_context 'with AWS kms_providers'
Expand Down
6 changes: 6 additions & 0 deletions spec/runners/unified/test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,12 @@ def generate_entities(es)
[ provider, converted_options ]
end.to_h

# A keyExpirationMS of 0 is meaningful (never expire), so test
# for presence rather than truthiness.
if client_encryption_opts.key?('keyExpirationMS')
opts[:key_expiration_ms] = client_encryption_opts['keyExpirationMS']
end

Mongo::ClientEncryption.new(
key_vault_client,
opts
Expand Down
117 changes: 117 additions & 0 deletions spec/spec_tests/data/client_side_encryption/unified/keyCache.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
description: keyCache-explicit

schemaVersion: "1.22"

runOnRequirements:
- csfle: true

createEntities:
- client:
id: &client0 client0
observeEvents:
- commandStartedEvent
- clientEncryption:
id: &clientEncryption0 clientEncryption0
clientEncryptionOpts:
keyVaultClient: *client0
keyVaultNamespace: keyvault.datakeys
kmsProviders:
"local":
{
key: "OCTP9uKPPmvuqpHlqq83gPk4U6rUPxKVRRyVtrjFmVjdoa4Xzm1SzUbr7aIhNI42czkUBmrCtZKF31eaaJnxEBkqf0RFukA9Mo3NEHQWgAQ2cn9duOcRbaFUQo2z0/rB",
}
keyExpirationMS: 1
- database:
id: &database0 database0
client: *client0
databaseName: &database0Name keyvault
- collection:
id: &collection0 collection0
database: *database0
collectionName: &collection0Name datakeys

initialData:
- databaseName: *database0Name
collectionName: *collection0Name
documents:
- {
"_id": { "$binary": { "base64": "a+YWzdygTAG62/cNUkqZiQ==", "subType": "04" } },
"keyAltNames": [],
"keyMaterial":
{
"$binary":
{
"base64": "iocBkhO3YBokiJ+FtxDTS71/qKXQ7tSWhWbcnFTXBcMjarsepvALeJ5li+SdUd9ePuatjidxAdMo7vh1V2ZESLMkQWdpPJ9PaJjA67gKQKbbbB4Ik5F2uKjULvrMBnFNVRMup4JNUwWFQJpqbfMveXnUVcD06+pUpAkml/f+DSXrV3e5rxciiNVtz03dAG8wJrsKsFXWj6vTjFhsfknyBA==",
"subType": "00",
},
},
"creationDate": { "$date": { "$numberLong": "1552949630483" } },
"updateDate": { "$date": { "$numberLong": "1552949630483" } },
"status": { "$numberInt": "0" },
"masterKey": { "provider": "local" },
}

tests:
- description: decrypt, wait, and decrypt again
operations:
- name: decrypt
object: *clientEncryption0
arguments:
value:
{
"$binary":
{
"base64": "AWvmFs3coEwButv3DVJKmYkCJ6lUzRX9R28WNlw5uyndb+8gurA+p8q14s7GZ04K2ZvghieRlAr5UwZbow3PMq27u5EIhDDczwBFcbdP1amllw==",
"subType": "06",
},
}
expectResult: "foobar"
- name: wait
object: testRunner
arguments:
ms: 50 # Wait long enough to account for coarse time resolution on Windows (CDRIVER-4526).
- name: decrypt
object: *clientEncryption0
arguments:
value:
{
"$binary":
{
"base64": "AWvmFs3coEwButv3DVJKmYkCJ6lUzRX9R28WNlw5uyndb+8gurA+p8q14s7GZ04K2ZvghieRlAr5UwZbow3PMq27u5EIhDDczwBFcbdP1amllw==",
"subType": "06",
},
}
expectResult: "foobar"
expectEvents:
- client: *client0
events:
- commandStartedEvent:
command:
find: datakeys
filter:
{
"$or":
[
{
"_id": { "$in": [{ "$binary": { "base64": "a+YWzdygTAG62/cNUkqZiQ==", "subType": "04" } }] },
},
{ "keyAltNames": { "$in": [] } },
],
}
$db: keyvault
readConcern: { level: "majority" }
- commandStartedEvent:
command:
find: datakeys
filter:
{
"$or":
[
{
"_id": { "$in": [{ "$binary": { "base64": "a+YWzdygTAG62/cNUkqZiQ==", "subType": "04" } }] },
},
{ "keyAltNames": { "$in": [] } },
],
}
$db: keyvault
readConcern: { level: "majority" }
Loading