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..5fd2d5ca73 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, errors.New("'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 a604c83d60..9baf785dcb 100644 --- a/internal/integration/unified/operation.go +++ b/internal/integration/unified/operation.go @@ -270,6 +270,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 4bff29d7c5..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.21", + 1: "1.22", } ) diff --git a/mongo/client.go b/mongo/client.go index 09535f2ba6..8fb93985fa 100644 --- a/mongo/client.go +++ b/mongo/client.go @@ -609,7 +609,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 } 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..64c7c90817 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,11 @@ func (a *AutoEncryptionOptions) SetBypassQueryAnalysis(bypass bool) *AutoEncrypt return a } + +// SetKeyExpiration specifies duration for the key expiration. 0 or negative value means "never expire". +// The granularity is in milliseconds. Any sub-millisecond fraction will be rounded up. +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..cd249d0e76 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,18 @@ func (c *ClientEncryptionOptionsBuilder) SetTLSConfig(cfg map[string]*tls.Config return c } +// SetKeyExpiration specifies duration for the key expiration. 0 or negative value means "never expire". +// The granularity is in milliseconds. Any sub-millisecond fraction will be rounded up. +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 7f7c3e8fc9..91b950c371 100644 --- a/x/mongo/driver/mongocrypt/mongocrypt.go +++ b/x/mongo/driver/mongocrypt/mongocrypt.go @@ -20,6 +20,7 @@ import ( "errors" "fmt" "net/http" + "time" "unsafe" "go.mongodb.org/mongo-driver/v2/bson" @@ -89,6 +90,17 @@ func NewMongoCrypt(opts *options.MongoCryptOptions) (*MongoCrypt, error) { C.mongocrypt_setopt_bypass_query_analysis(crypt.wrapped) } + var keyExpirationMs uint64 = 60_000 // 60,000 ms + if opts.KeyExpiration != nil { + if *opts.KeyExpiration <= 0 { + keyExpirationMs = 0 + } else { + // find the ceiling integer millisecond for the expiration + keyExpirationMs = uint64((*opts.KeyExpiration + time.Millisecond - 1) / time.Millisecond) + } + } + 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..504065d4bf 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,10 @@ func (mo *MongoCryptOptions) SetHTTPClient(httpClient *http.Client) *MongoCryptO mo.HTTPClient = httpClient return mo } + +// SetKeyExpiration sets the key expiration duration. 0 means "never expire". +// The granularity is in milliseconds. Any sub-millisecond fraction will be rounded up. +func (mo *MongoCryptOptions) SetKeyExpiration(expiration *time.Duration) *MongoCryptOptions { + mo.KeyExpiration = expiration + return mo +}