From 32e7246685d14765cbe8d2d3968193c340ba6725 Mon Sep 17 00:00:00 2001 From: Qingyang Hu Date: Fri, 17 Jan 2025 16:46:25 -0500 Subject: [PATCH] GODRIVER-3289 Add option to configure DEK cache lifetime. --- etc/install-libmongocrypt.sh | 2 +- internal/integration/json_helpers_test.go | 2 + .../client_encryption_operation_execution.go | 23 ++ internal/integration/unified/entity.go | 16 +- internal/integration/unified/operation.go | 2 + .../integration/unified/schema_version.go | 2 +- mongo/client.go | 81 ++---- mongo/client_encryption.go | 3 +- mongo/options/autoencryptionoptions.go | 9 + mongo/options/clientencryptionoptions.go | 13 + .../legacy/keyCache.json | 270 ++++++++++++++++++ .../legacy/keyCache.yml | 69 +++++ .../unified/keyCache.json | 198 +++++++++++++ .../unified/keyCache.yml | 85 ++++++ x/mongo/driver/mongocrypt/mongocrypt.go | 11 + .../mongocrypt/options/mongocrypt_options.go | 8 + 16 files changed, 734 insertions(+), 60 deletions(-) create mode 100644 testdata/client-side-encryption/legacy/keyCache.json create mode 100644 testdata/client-side-encryption/legacy/keyCache.yml create mode 100644 testdata/client-side-encryption/unified/keyCache.json create mode 100644 testdata/client-side-encryption/unified/keyCache.yml diff --git a/etc/install-libmongocrypt.sh b/etc/install-libmongocrypt.sh index 646721a8f7..a94d648eae 100755 --- a/etc/install-libmongocrypt.sh +++ b/etc/install-libmongocrypt.sh @@ -3,7 +3,7 @@ # This script installs libmongocrypt into an "install" directory. set -eux -LIBMONGOCRYPT_TAG="1.11.0" +LIBMONGOCRYPT_TAG="1.12.0" # Install libmongocrypt based on OS. if [ "Windows_NT" = "${OS:-}" ]; then diff --git a/internal/integration/json_helpers_test.go b/internal/integration/json_helpers_test.go index e3ccb5254e..fbd71083d7 100644 --- a/internal/integration/json_helpers_test.go +++ b/internal/integration/json_helpers_test.go @@ -162,6 +162,8 @@ func createAutoEncryptionOptions(t testing.TB, opts bson.Raw) *options.AutoEncry aeo.SetEncryptedFieldsMap(encryptedFieldsMap) case "bypassQueryAnalysis": aeo.SetBypassQueryAnalysis(opt.Boolean()) + case "keyExpirationMS": + aeo.SetKeyExpiration(time.Duration(opt.Int32()) * time.Millisecond) default: t.Fatalf("unrecognized auto encryption option: %v", name) } diff --git a/internal/integration/unified/client_encryption_operation_execution.go b/internal/integration/unified/client_encryption_operation_execution.go index ed2f76a6a8..cc8f5972fd 100644 --- a/internal/integration/unified/client_encryption_operation_execution.go +++ b/internal/integration/unified/client_encryption_operation_execution.go @@ -391,3 +391,26 @@ func executeRewrapManyDataKey(ctx context.Context, operation *operation) (*opera } return rewrapManyDataKeyResultsOpResult(result) } + +// executeDecrypt will decrypt the given value. +func executeDecrypt(ctx context.Context, operation *operation) (*operationResult, error) { + cee, err := entities(ctx).clientEncryption(operation.Object) + if err != nil { + return nil, err + } + + rawValue, err := operation.Arguments.LookupErr("value") + if err != nil { + return nil, err + } + t, d, ok := rawValue.BinaryOK() + if !ok { + return nil, fmt.Errorf("'value' argument is not a BSON binary") + } + + rawValue, err = cee.Decrypt(ctx, bson.Binary{Subtype: t, Data: d}) + if err != nil { + return newErrorResult(err), nil + } + return newValueResult(rawValue.Type, rawValue.Value, err), nil +} diff --git a/internal/integration/unified/entity.go b/internal/integration/unified/entity.go index 779570a8da..4aacc44a73 100644 --- a/internal/integration/unified/entity.go +++ b/internal/integration/unified/entity.go @@ -179,6 +179,7 @@ type clientEncryptionOpts struct { KeyVaultClient string `bson:"keyVaultClient"` KeyVaultNamespace string `bson:"keyVaultNamespace"` KmsProviders map[string]bson.Raw `bson:"kmsProviders"` + KeyExpirationMS *int64 `bson:"keyExpirationMS"` } // EntityMap is used to store entities during tests. This type enforces uniqueness so no two entities can have the same @@ -735,12 +736,15 @@ func (em *EntityMap) addClientEncryptionEntity(entityOptions *entityOptions) err return newEntityNotFoundError("client", ceo.KeyVaultClient) } - ce, err := mongo.NewClientEncryption( - keyVaultClient.Client, - options.ClientEncryption(). - SetKeyVaultNamespace(ceo.KeyVaultNamespace). - SetTLSConfig(tlsconf). - SetKmsProviders(kmsProviders)) + opts := options.ClientEncryption(). + SetKeyVaultNamespace(ceo.KeyVaultNamespace). + SetTLSConfig(tlsconf). + SetKmsProviders(kmsProviders) + if ceo.KeyExpirationMS != nil { + opts.SetKeyExpiration(time.Duration(*ceo.KeyExpirationMS) * time.Millisecond) + } + + ce, err := mongo.NewClientEncryption(keyVaultClient.Client, opts) if err != nil { return err } diff --git a/internal/integration/unified/operation.go b/internal/integration/unified/operation.go index f74fc9361e..31d5037fce 100644 --- a/internal/integration/unified/operation.go +++ b/internal/integration/unified/operation.go @@ -268,6 +268,8 @@ func (op *operation) run(ctx context.Context, loopDone <-chan struct{}) (*operat return executeDeleteKey(ctx, op) case "addKeyAltName": return executeAddKeyAltName(ctx, op) + case "decrypt": + return executeDecrypt(ctx, op) // Unsupported operations case "count", "listIndexNames": diff --git a/internal/integration/unified/schema_version.go b/internal/integration/unified/schema_version.go index 1d2f99fb96..7908b39017 100644 --- a/internal/integration/unified/schema_version.go +++ b/internal/integration/unified/schema_version.go @@ -16,7 +16,7 @@ import ( var ( supportedSchemaVersions = map[int]string{ - 1: "1.17", + 1: "1.22", } ) diff --git a/mongo/client.go b/mongo/client.go index df190b7450..44332be2df 100644 --- a/mongo/client.go +++ b/mongo/client.go @@ -193,7 +193,7 @@ func newClient(opts ...*options.ClientOptions) (*Client, error) { } // AutoEncryptionOptions if clientOpts.AutoEncryptionOptions != nil { - if err := client.configureAutoEncryption(clientOpts); err != nil { + if err = client.configureAutoEncryption(clientOpts); err != nil { return nil, err } } else { @@ -471,30 +471,48 @@ func (c *Client) endSessions(ctx context.Context) { } func (c *Client) configureAutoEncryption(args *options.ClientOptions) error { - c.encryptedFieldsMap = args.AutoEncryptionOptions.EncryptedFieldsMap + aeOpts := args.AutoEncryptionOptions + c.encryptedFieldsMap = aeOpts.EncryptedFieldsMap if err := c.configureKeyVaultClientFLE(args); err != nil { return err } - if err := c.configureMetadataClientFLE(args); err != nil { - return err - } - - mc, err := c.newMongoCrypt(args.AutoEncryptionOptions) + mc, err := c.newMongoCrypt(aeOpts) if err != nil { return err } // If the crypt_shared library was not loaded, try to spawn and connect to mongocryptd. if mc.CryptSharedLibVersionString() == "" { - mongocryptdFLE, err := newMongocryptdClient(args.AutoEncryptionOptions) + c.mongocryptdFLE, err = newMongocryptdClient(aeOpts) if err != nil { return err } - c.mongocryptdFLE = mongocryptdFLE } - c.configureCryptFLE(mc, args.AutoEncryptionOptions) + kr := keyRetriever{coll: c.keyVaultCollFLE} + var cir collInfoRetriever + bypass := aeOpts.BypassAutoEncryption != nil && *aeOpts.BypassAutoEncryption + if !bypass { + if args.MaxPoolSize != nil && *args.MaxPoolSize == 0 { + c.metadataClientFLE = c + } else { + c.metadataClientFLE, err = c.getOrCreateInternalClient(args) + if err != nil { + return err + } + } + cir.client = c.metadataClientFLE + } + + c.cryptFLE = driver.NewCrypt(&driver.CryptOptions{ + MongoCrypt: mc, + CollInfoFn: cir.cryptCollInfo, + KeyFn: kr.cryptKeys, + MarkFn: c.mongocryptdFLE.markCommand, + TLSConfig: aeOpts.TLSConfig, + BypassAutoEncryption: bypass, + }) return nil } @@ -537,24 +555,6 @@ func (c *Client) configureKeyVaultClientFLE(clientOpts *options.ClientOptions) e return nil } -func (c *Client) configureMetadataClientFLE(clientOpts *options.ClientOptions) error { - aeOpts := clientOpts.AutoEncryptionOptions - - if aeOpts.BypassAutoEncryption != nil && *aeOpts.BypassAutoEncryption { - // no need for a metadata client. - return nil - } - if clientOpts.MaxPoolSize != nil && *clientOpts.MaxPoolSize == 0 { - c.metadataClientFLE = c - return nil - } - - var err error - c.metadataClientFLE, err = c.getOrCreateInternalClient(clientOpts) - - return err -} - func (c *Client) newMongoCrypt(opts *options.AutoEncryptionOptions) (*mongocrypt.MongoCrypt, error) { // convert schemas in SchemaMap to bsoncore documents cryptSchemaMap := make(map[string]bsoncore.Document) @@ -611,7 +611,8 @@ func (c *Client) newMongoCrypt(opts *options.AutoEncryptionOptions) (*mongocrypt SetEncryptedFieldsMap(cryptEncryptedFieldsMap). SetCryptSharedLibDisabled(cryptSharedLibDisabled || bypassAutoEncryption). SetCryptSharedLibOverridePath(cryptSharedLibPath). - SetHTTPClient(opts.HTTPClient)) + SetHTTPClient(opts.HTTPClient). + SetKeyExpiration(opts.KeyExpiration)) if err != nil { return nil, err } @@ -637,28 +638,6 @@ func (c *Client) newMongoCrypt(opts *options.AutoEncryptionOptions) (*mongocrypt return mc, nil } -//nolint:unused // the unused linter thinks that this function is unreachable because "c.newMongoCrypt" always panics without the "cse" build tag set. -func (c *Client) configureCryptFLE(mc *mongocrypt.MongoCrypt, opts *options.AutoEncryptionOptions) { - bypass := opts.BypassAutoEncryption != nil && *opts.BypassAutoEncryption - kr := keyRetriever{coll: c.keyVaultCollFLE} - var cir collInfoRetriever - // If bypass is true, c.metadataClientFLE is nil and the collInfoRetriever - // will not be used. If bypass is false, to the parent client or the - // internal client. - if !bypass { - cir = collInfoRetriever{client: c.metadataClientFLE} - } - - c.cryptFLE = driver.NewCrypt(&driver.CryptOptions{ - MongoCrypt: mc, - CollInfoFn: cir.cryptCollInfo, - KeyFn: kr.cryptKeys, - MarkFn: c.mongocryptdFLE.markCommand, - TLSConfig: opts.TLSConfig, - BypassAutoEncryption: bypass, - }) -} - // validSession returns an error if the session doesn't belong to the client func (c *Client) validSession(sess *session.Client) error { if sess != nil && sess.ClientID != c.id { diff --git a/mongo/client_encryption.go b/mongo/client_encryption.go index 07c18529a8..dc48542977 100644 --- a/mongo/client_encryption.go +++ b/mongo/client_encryption.go @@ -59,7 +59,8 @@ func NewClientEncryption(keyVaultClient *Client, opts ...options.Lister[options. // ClientEncryption because it's only needed for AutoEncryption and we don't expect users to // have the crypt_shared library installed if they're using ClientEncryption. SetCryptSharedLibDisabled(true). - SetHTTPClient(cea.HTTPClient)) + SetHTTPClient(cea.HTTPClient). + SetKeyExpiration(cea.KeyExpiration)) if err != nil { return nil, err } diff --git a/mongo/options/autoencryptionoptions.go b/mongo/options/autoencryptionoptions.go index c630659c5a..3fb2db46c6 100644 --- a/mongo/options/autoencryptionoptions.go +++ b/mongo/options/autoencryptionoptions.go @@ -9,6 +9,7 @@ package options import ( "crypto/tls" "net/http" + "time" "go.mongodb.org/mongo-driver/v2/internal/httputil" ) @@ -40,6 +41,7 @@ type AutoEncryptionOptions struct { HTTPClient *http.Client EncryptedFieldsMap map[string]interface{} BypassQueryAnalysis *bool + KeyExpiration *time.Duration } // AutoEncryption creates a new AutoEncryptionOptions configured with default values. @@ -164,3 +166,10 @@ func (a *AutoEncryptionOptions) SetBypassQueryAnalysis(bypass bool) *AutoEncrypt return a } + +// SetKeyExpiration specifies duration for the key expiration. 0 means "never expire". +func (a *AutoEncryptionOptions) SetKeyExpiration(expiration time.Duration) *AutoEncryptionOptions { + a.KeyExpiration = &expiration + + return a +} diff --git a/mongo/options/clientencryptionoptions.go b/mongo/options/clientencryptionoptions.go index 3f9b3745ed..802b90d82d 100644 --- a/mongo/options/clientencryptionoptions.go +++ b/mongo/options/clientencryptionoptions.go @@ -10,6 +10,7 @@ import ( "crypto/tls" "fmt" "net/http" + "time" "go.mongodb.org/mongo-driver/v2/internal/httputil" ) @@ -22,6 +23,7 @@ type ClientEncryptionOptions struct { KmsProviders map[string]map[string]interface{} TLSConfig map[string]*tls.Config HTTPClient *http.Client + KeyExpiration *time.Duration } // ClientEncryptionOptionsBuilder contains options to configure client @@ -80,6 +82,17 @@ func (c *ClientEncryptionOptionsBuilder) SetTLSConfig(cfg map[string]*tls.Config return c } +// SetKeyExpiration specifies duration for the key expiration. 0 means "never expire". +func (c *ClientEncryptionOptionsBuilder) SetKeyExpiration(expiration time.Duration) *ClientEncryptionOptionsBuilder { + c.Opts = append(c.Opts, func(opts *ClientEncryptionOptions) error { + opts.KeyExpiration = &expiration + + return nil + }) + + return c +} + // BuildTLSConfig specifies tls.Config options for each KMS provider to use to configure TLS on all connections created // to the KMS provider. The input map should contain a mapping from each KMS provider to a document containing the necessary // options, as follows: diff --git a/testdata/client-side-encryption/legacy/keyCache.json b/testdata/client-side-encryption/legacy/keyCache.json new file mode 100644 index 0000000000..912ce80020 --- /dev/null +++ b/testdata/client-side-encryption/legacy/keyCache.json @@ -0,0 +1,270 @@ +{ + "runOn": [ + { + "minServerVersion": "4.1.10" + } + ], + "database_name": "default", + "collection_name": "default", + "data": [], + "json_schema": { + "properties": { + "encrypted_w_altname": { + "encrypt": { + "keyId": "/altname", + "bsonType": "string", + "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random" + } + }, + "encrypted_string": { + "encrypt": { + "keyId": [ + { + "$binary": { + "base64": "AAAAAAAAAAAAAAAAAAAAAA==", + "subType": "04" + } + } + ], + "bsonType": "string", + "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic" + } + }, + "random": { + "encrypt": { + "keyId": [ + { + "$binary": { + "base64": "AAAAAAAAAAAAAAAAAAAAAA==", + "subType": "04" + } + } + ], + "bsonType": "string", + "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random" + } + }, + "encrypted_string_equivalent": { + "encrypt": { + "keyId": [ + { + "$binary": { + "base64": "AAAAAAAAAAAAAAAAAAAAAA==", + "subType": "04" + } + } + ], + "bsonType": "string", + "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic" + } + } + }, + "bsonType": "object" + }, + "key_vault_data": [ + { + "status": 1, + "_id": { + "$binary": { + "base64": "AAAAAAAAAAAAAAAAAAAAAA==", + "subType": "04" + } + }, + "masterKey": { + "provider": "aws", + "key": "arn:aws:kms:us-east-1:579766882180:key/89fcc2c4-08b0-4bd9-9f25-e30687b580d0", + "region": "us-east-1" + }, + "updateDate": { + "$date": { + "$numberLong": "1552949630483" + } + }, + "keyMaterial": { + "$binary": { + "base64": "AQICAHhQNmWG2CzOm1dq3kWLM+iDUZhEqnhJwH9wZVpuZ94A8gEqnsxXlR51T5EbEVezUqqKAAAAwjCBvwYJKoZIhvcNAQcGoIGxMIGuAgEAMIGoBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDHa4jo6yp0Z18KgbUgIBEIB74sKxWtV8/YHje5lv5THTl0HIbhSwM6EqRlmBiFFatmEWaeMk4tO4xBX65eq670I5TWPSLMzpp8ncGHMmvHqRajNBnmFtbYxN3E3/WjxmdbOOe+OXpnGJPcGsftc7cB2shRfA4lICPnE26+oVNXT6p0Lo20nY5XC7jyCO", + "subType": "00" + } + }, + "creationDate": { + "$date": { + "$numberLong": "1552949630483" + } + }, + "keyAltNames": [ + "altname", + "another_altname" + ] + } + ], + "tests": [ + { + "description": "Insert with deterministic encryption, then find it", + "clientOptions": { + "autoEncryptOpts": { + "kmsProviders": { + "aws": {} + }, + "keyExpirationMS": 1 + } + }, + "operations": [ + { + "name": "insertOne", + "arguments": { + "document": { + "_id": 1, + "encrypted_string": "string0" + } + } + }, + { + "name": "wait", + "object": "testRunner", + "arguments": { + "ms": 50 + } + }, + { + "name": "find", + "arguments": { + "filter": { + "_id": 1 + } + }, + "result": [ + { + "_id": 1, + "encrypted_string": "string0" + } + ] + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "listCollections": 1, + "filter": { + "name": "default" + } + }, + "command_name": "listCollections" + } + }, + { + "command_started_event": { + "command": { + "find": "datakeys", + "filter": { + "$or": [ + { + "_id": { + "$in": [ + { + "$binary": { + "base64": "AAAAAAAAAAAAAAAAAAAAAA==", + "subType": "04" + } + } + ] + } + }, + { + "keyAltNames": { + "$in": [] + } + } + ] + }, + "$db": "keyvault", + "readConcern": { + "level": "majority" + } + }, + "command_name": "find" + } + }, + { + "command_started_event": { + "command": { + "insert": "default", + "documents": [ + { + "_id": 1, + "encrypted_string": { + "$binary": { + "base64": "AQAAAAAAAAAAAAAAAAAAAAACwj+3zkv2VM+aTfk60RqhXq6a/77WlLwu/BxXFkL7EppGsju/m8f0x5kBDD3EZTtGALGXlym5jnpZAoSIkswHoA==", + "subType": "06" + } + } + } + ], + "ordered": true + }, + "command_name": "insert" + } + }, + { + "command_started_event": { + "command": { + "find": "default", + "filter": { + "_id": 1 + } + }, + "command_name": "find" + } + }, + { + "command_started_event": { + "command": { + "find": "datakeys", + "filter": { + "$or": [ + { + "_id": { + "$in": [ + { + "$binary": { + "base64": "AAAAAAAAAAAAAAAAAAAAAA==", + "subType": "04" + } + } + ] + } + }, + { + "keyAltNames": { + "$in": [] + } + } + ] + }, + "$db": "keyvault", + "readConcern": { + "level": "majority" + } + }, + "command_name": "find" + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "encrypted_string": { + "$binary": { + "base64": "AQAAAAAAAAAAAAAAAAAAAAACwj+3zkv2VM+aTfk60RqhXq6a/77WlLwu/BxXFkL7EppGsju/m8f0x5kBDD3EZTtGALGXlym5jnpZAoSIkswHoA==", + "subType": "06" + } + } + } + ] + } + } + } + ] +} diff --git a/testdata/client-side-encryption/legacy/keyCache.yml b/testdata/client-side-encryption/legacy/keyCache.yml new file mode 100644 index 0000000000..28acf7a2ee --- /dev/null +++ b/testdata/client-side-encryption/legacy/keyCache.yml @@ -0,0 +1,69 @@ +runOn: + - minServerVersion: "4.1.10" +database_name: &database_name "default" +collection_name: &collection_name "default" + +data: [] +json_schema: {'properties': {'encrypted_w_altname': {'encrypt': {'keyId': '/altname', 'bsonType': 'string', 'algorithm': 'AEAD_AES_256_CBC_HMAC_SHA_512-Random'}}, 'encrypted_string': {'encrypt': {'keyId': [{'$binary': {'base64': 'AAAAAAAAAAAAAAAAAAAAAA==', 'subType': '04'}}], 'bsonType': 'string', 'algorithm': 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'}}, 'random': {'encrypt': {'keyId': [{'$binary': {'base64': 'AAAAAAAAAAAAAAAAAAAAAA==', 'subType': '04'}}], 'bsonType': 'string', 'algorithm': 'AEAD_AES_256_CBC_HMAC_SHA_512-Random'}}, 'encrypted_string_equivalent': {'encrypt': {'keyId': [{'$binary': {'base64': 'AAAAAAAAAAAAAAAAAAAAAA==', 'subType': '04'}}], 'bsonType': 'string', 'algorithm': 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'}}}, 'bsonType': 'object'} +key_vault_data: [{'status': 1, '_id': {'$binary': {'base64': 'AAAAAAAAAAAAAAAAAAAAAA==', 'subType': '04'}}, 'masterKey': {'provider': 'aws', 'key': 'arn:aws:kms:us-east-1:579766882180:key/89fcc2c4-08b0-4bd9-9f25-e30687b580d0', 'region': 'us-east-1'}, 'updateDate': {'$date': {'$numberLong': '1552949630483'}}, 'keyMaterial': {'$binary': {'base64': 'AQICAHhQNmWG2CzOm1dq3kWLM+iDUZhEqnhJwH9wZVpuZ94A8gEqnsxXlR51T5EbEVezUqqKAAAAwjCBvwYJKoZIhvcNAQcGoIGxMIGuAgEAMIGoBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDHa4jo6yp0Z18KgbUgIBEIB74sKxWtV8/YHje5lv5THTl0HIbhSwM6EqRlmBiFFatmEWaeMk4tO4xBX65eq670I5TWPSLMzpp8ncGHMmvHqRajNBnmFtbYxN3E3/WjxmdbOOe+OXpnGJPcGsftc7cB2shRfA4lICPnE26+oVNXT6p0Lo20nY5XC7jyCO', 'subType': '00'}}, 'creationDate': {'$date': {'$numberLong': '1552949630483'}}, 'keyAltNames': ['altname', 'another_altname']}] + +tests: + - description: "Insert with deterministic encryption, then find it" + clientOptions: + autoEncryptOpts: + kmsProviders: + aws: {} # Credentials filled in from environment. + keyExpirationMS: 1 + operations: + - name: insertOne + arguments: + document: &doc0 { _id: 1, encrypted_string: "string0" } + - name: wait + object: testRunner + arguments: + ms: 50 # Wait long enough to account for coarse time resolution on Windows (CDRIVER-4526). + - name: find + arguments: + filter: { _id: 1 } + result: [*doc0] + expectations: + # Auto encryption will request the collection info. + - command_started_event: + command: + listCollections: 1 + filter: + name: *collection_name + command_name: listCollections + # Then key is fetched from the key vault. + - command_started_event: + command: + find: datakeys + filter: {"$or": [{"_id": {"$in": [ {'$binary': {'base64': 'AAAAAAAAAAAAAAAAAAAAAA==', 'subType': '04'}} ] }}, {"keyAltNames": {"$in": []}}]} + $db: keyvault + readConcern: { level: "majority" } + command_name: find + - command_started_event: + command: + insert: *collection_name + documents: + - &doc0_encrypted { _id: 1, encrypted_string: {'$binary': {'base64': 'AQAAAAAAAAAAAAAAAAAAAAACwj+3zkv2VM+aTfk60RqhXq6a/77WlLwu/BxXFkL7EppGsju/m8f0x5kBDD3EZTtGALGXlym5jnpZAoSIkswHoA==', 'subType': '06'}} } + ordered: true + command_name: insert + - command_started_event: + command: + find: *collection_name + filter: { _id: 1 } + command_name: find + # The cache has expired and the key must be fetched again + - command_started_event: + command: + find: datakeys + filter: {"$or": [{"_id": {"$in": [ {'$binary': {'base64': 'AAAAAAAAAAAAAAAAAAAAAA==', 'subType': '04'}} ] }}, {"keyAltNames": {"$in": []}}]} + $db: keyvault + readConcern: { level: "majority" } + command_name: find + outcome: + collection: + # Outcome is checked using a separate MongoClient without auto encryption. + data: + - *doc0_encrypted diff --git a/testdata/client-side-encryption/unified/keyCache.json b/testdata/client-side-encryption/unified/keyCache.json new file mode 100644 index 0000000000..a39701e286 --- /dev/null +++ b/testdata/client-side-encryption/unified/keyCache.json @@ -0,0 +1,198 @@ +{ + "description": "keyCache-explicit", + "schemaVersion": "1.22", + "runOnRequirements": [ + { + "csfle": true + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "clientEncryption": { + "id": "clientEncryption0", + "clientEncryptionOpts": { + "keyVaultClient": "client0", + "keyVaultNamespace": "keyvault.datakeys", + "kmsProviders": { + "local": { + "key": "OCTP9uKPPmvuqpHlqq83gPk4U6rUPxKVRRyVtrjFmVjdoa4Xzm1SzUbr7aIhNI42czkUBmrCtZKF31eaaJnxEBkqf0RFukA9Mo3NEHQWgAQ2cn9duOcRbaFUQo2z0/rB" + } + }, + "keyExpirationMS": 1 + } + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "keyvault" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "datakeys" + } + } + ], + "initialData": [ + { + "databaseName": "keyvault", + "collectionName": "datakeys", + "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 + } + }, + { + "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" + } + } + } + } + ] + } + ] + } + ] +} diff --git a/testdata/client-side-encryption/unified/keyCache.yml b/testdata/client-side-encryption/unified/keyCache.yml new file mode 100644 index 0000000000..d6e747ba09 --- /dev/null +++ b/testdata/client-side-encryption/unified/keyCache.yml @@ -0,0 +1,85 @@ +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" } diff --git a/x/mongo/driver/mongocrypt/mongocrypt.go b/x/mongo/driver/mongocrypt/mongocrypt.go index 5f34f5cd71..08ebe4676d 100644 --- a/x/mongo/driver/mongocrypt/mongocrypt.go +++ b/x/mongo/driver/mongocrypt/mongocrypt.go @@ -88,6 +88,17 @@ func NewMongoCrypt(opts *options.MongoCryptOptions) (*MongoCrypt, error) { C.mongocrypt_setopt_bypass_query_analysis(wrapped) } + var keyExpirationMs uint64 = 60_000 // 60,000 ms + if opts.KeyExpiration != nil { + expirationMs := opts.KeyExpiration.Milliseconds() + if expirationMs < 0 { + keyExpirationMs = 0 + } else { + keyExpirationMs = uint64(expirationMs) + } + } + C.mongocrypt_setopt_key_expiration(crypt.wrapped, C.uint64_t(keyExpirationMs)) + // If loading the crypt_shared library isn't disabled, set the default library search path "$SYSTEM" // and set a library override path if one was provided. if !opts.CryptSharedLibDisabled { diff --git a/x/mongo/driver/mongocrypt/options/mongocrypt_options.go b/x/mongo/driver/mongocrypt/options/mongocrypt_options.go index c6474a4b0b..7a4432af26 100644 --- a/x/mongo/driver/mongocrypt/options/mongocrypt_options.go +++ b/x/mongo/driver/mongocrypt/options/mongocrypt_options.go @@ -8,6 +8,7 @@ package options import ( "net/http" + "time" "go.mongodb.org/mongo-driver/v2/x/bsonx/bsoncore" ) @@ -21,6 +22,7 @@ type MongoCryptOptions struct { CryptSharedLibDisabled bool CryptSharedLibOverridePath string HTTPClient *http.Client + KeyExpiration *time.Duration } // MongoCrypt creates a new MongoCryptOptions instance. @@ -70,3 +72,9 @@ func (mo *MongoCryptOptions) SetHTTPClient(httpClient *http.Client) *MongoCryptO mo.HTTPClient = httpClient return mo } + +// SetKeyExpiration sets the key expiration duration. 0 means "never expire". +func (mo *MongoCryptOptions) SetKeyExpiration(expiration *time.Duration) *MongoCryptOptions { + mo.KeyExpiration = expiration + return mo +}