From 4a1e55d2d007934ad5578651e74c0d777ab3818b Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Fri, 30 Jun 2023 23:59:57 -0400 Subject: [PATCH 1/6] PHPLIB-1176: Various improvements for In-Use Encryption tutorial Adds additional non-enterprise examples from the PyMongo tutorial: "Explicit Encryption with Automatic Decryption" and "Explicit Queryable Encryption". Examples are now broken out into separate files, which are tested in ExamplesTest. Renames "Client-Side Encryption" to "In-Use Encryption" (PHPLIB-997). This will warrant adding a redirect from "/tutorial/client-side-encryption/" to "/tutorial/encryption/" in the related docs-php-library project. Adds docs for crypt_shared and mongocryptd (PHPLIB-985). --- docs/examples/create_data_key.php | 39 ++ ...sfle-automatic_encryption-local_schema.php | 72 ++++ ...utomatic_encryption-server_side_schema.php | 73 ++++ docs/examples/csfle-explicit_encryption.php | 48 +++ ...plicit_encryption_automatic_decryption.php | 52 +++ docs/examples/key_alt_name.php | 37 ++ .../queryable_encryption-automatic.php | 79 ++++ .../queryable_encryption-explicit.php | 94 +++++ docs/tutorial.txt | 2 +- docs/tutorial/client-side-encryption.txt | 368 ------------------ docs/tutorial/encryption.txt | 265 +++++++++++++ phpcs.xml.dist | 1 + psalm-baseline.xml | 58 +++ psalm.xml.dist | 1 + tests/ExamplesTest.php | 275 ++++++++++++- 15 files changed, 1083 insertions(+), 381 deletions(-) create mode 100644 docs/examples/create_data_key.php create mode 100644 docs/examples/csfle-automatic_encryption-local_schema.php create mode 100644 docs/examples/csfle-automatic_encryption-server_side_schema.php create mode 100644 docs/examples/csfle-explicit_encryption.php create mode 100644 docs/examples/csfle-explicit_encryption_automatic_decryption.php create mode 100644 docs/examples/key_alt_name.php create mode 100644 docs/examples/queryable_encryption-automatic.php create mode 100644 docs/examples/queryable_encryption-explicit.php delete mode 100644 docs/tutorial/client-side-encryption.txt create mode 100644 docs/tutorial/encryption.txt diff --git a/docs/examples/create_data_key.php b/docs/examples/create_data_key.php new file mode 100644 index 000000000..9de810d07 --- /dev/null +++ b/docs/examples/create_data_key.php @@ -0,0 +1,39 @@ +createClientEncryption([ + 'keyVaultNamespace' => 'encryption.__keyVault', + 'kmsProviders' => [ + 'local' => ['key' => $localKey], + ], +]); + +/* Drop the key vault collection and create an encryption key. This would + * typically be done during application deployment. To store the key ID for + * later use, you can use serialize() or var_export(). */ +$client->selectCollection('encryption', '__keyVault')->drop(); +$keyId = $clientEncryption->createDataKey('local'); + +print_r($keyId); + +// Encrypt a value using the key that was just created +$encryptedValue = $clientEncryption->encrypt('mySecret', [ + 'algorithm' => ClientEncryption::AEAD_AES_256_CBC_HMAC_SHA_512_DETERMINISTIC, + 'keyId' => $keyId, +]); + +print_r($encryptedValue); diff --git a/docs/examples/csfle-automatic_encryption-local_schema.php b/docs/examples/csfle-automatic_encryption-local_schema.php new file mode 100644 index 000000000..d61be162e --- /dev/null +++ b/docs/examples/csfle-automatic_encryption-local_schema.php @@ -0,0 +1,72 @@ +createClientEncryption([ + 'keyVaultNamespace' => 'encryption.__keyVault', + 'kmsProviders' => [ + 'local' => ['key' => $localKey], + ], +]); + +/* Drop the key vault collection and create an encryption key. Alternatively, + * this key ID could be read from a configuration file. */ +$client->selectCollection('encryption', '__keyVault')->drop(); +$keyId = $clientEncryption->createDataKey('local'); + +/* Create a client with automatic encryption enabled. Specify a schemaMap option + * to enforce a local JSON schema. */ +$encryptedClient = new Client($uri, [], [ + 'autoEncryption' => [ + 'keyVaultNamespace' => 'encryption.__keyVault', + 'kmsProviders' => ['local' => ['key' => $localKey]], + 'schemaMap' => [ + 'test.coll' => [ + 'bsonType' => 'object', + 'properties' => [ + 'encryptedField' => [ + 'encrypt' => [ + 'keyId' => [$keyId], + 'bsonType' => 'string', + 'algorithm' => ClientEncryption::AEAD_AES_256_CBC_HMAC_SHA_512_DETERMINISTIC, + ], + ], + ], + ], + ], + ], +]); + +// Drop and create the collection. +$encryptedClient->selectDatabase('test')->dropCollection('coll'); +$encryptedClient->selectDatabase('test')->createCollection('coll'); +$encryptedCollection = $encryptedClient->selectCollection('test', 'coll'); + +/* Using the encrypted client, insert and find a document. The encrypted field + * will be automatically encrypted and decrypted. */ +$encryptedCollection->insertOne([ + '_id' => 1, + 'encryptedField' => 'mySecret', +]); + +print_r($encryptedCollection->findOne(['_id' => 1])); + +/* Using the client configured without encryption, find the same document and + * observe that the field is not automatically decrypted. Additionally, the JSON + * schema will prohibit inserting a document with an unencrypted field value. */ +$unencryptedCollection = $client->selectCollection('test', 'coll'); + +print_r($unencryptedCollection->findOne(['_id' => 1])); diff --git a/docs/examples/csfle-automatic_encryption-server_side_schema.php b/docs/examples/csfle-automatic_encryption-server_side_schema.php new file mode 100644 index 000000000..0474da7a2 --- /dev/null +++ b/docs/examples/csfle-automatic_encryption-server_side_schema.php @@ -0,0 +1,73 @@ +createClientEncryption([ + 'keyVaultNamespace' => 'encryption.__keyVault', + 'kmsProviders' => [ + 'local' => ['key' => $localKey], + ], +]); + +/* Drop the key vault collection and create an encryption key. Alternatively, + * this key ID could be read from a configuration file. */ +$client->selectCollection('encryption', '__keyVault')->drop(); +$keyId = $clientEncryption->createDataKey('local'); + +// Create a client with automatic encryption enabled +$encryptedClient = new Client($uri, [], [ + 'autoEncryption' => [ + 'keyVaultNamespace' => 'encryption.__keyVault', + 'kmsProviders' => ['local' => ['key' => $localKey]], + ], +]); + +/* Drop and create the collection. Specify a validator option when creating the + * collection to enforce a server-side JSON schema. */ +$validator = [ + '$jsonSchema' => [ + 'bsonType' => 'object', + 'properties' => [ + 'encryptedField' => [ + 'encrypt' => [ + 'keyId' => [$keyId], + 'bsonType' => 'string', + 'algorithm' => ClientEncryption::AEAD_AES_256_CBC_HMAC_SHA_512_DETERMINISTIC, + ], + ], + ], + ], +]; + +$encryptedClient->selectDatabase('test')->dropCollection('coll'); +$encryptedClient->selectDatabase('test')->createCollection('coll', ['validator' => $validator]); +$encryptedCollection = $encryptedClient->selectCollection('test', 'coll'); + +/* Using the encrypted client, insert and find a document. The encrypted field + * will be automatically encrypted and decrypted. */ +$encryptedCollection->insertOne([ + '_id' => 1, + 'encryptedField' => 'mySecret', +]); + +print_r($encryptedCollection->findOne(['_id' => 1])); + +/* Using the client configured without encryption, find the same document and + * observe that the field is not automatically decrypted. Additionally, the JSON + * schema will prohibit inserting a document with an unencrypted field value. */ +$unencryptedCollection = $client->selectCollection('test', 'coll'); + +print_r($unencryptedCollection->findOne(['_id' => 1])); diff --git a/docs/examples/csfle-explicit_encryption.php b/docs/examples/csfle-explicit_encryption.php new file mode 100644 index 000000000..34fc5b81c --- /dev/null +++ b/docs/examples/csfle-explicit_encryption.php @@ -0,0 +1,48 @@ +createClientEncryption([ + 'keyVaultNamespace' => 'encryption.__keyVault', + 'kmsProviders' => [ + 'local' => ['key' => $localKey], + ], +]); + +/* Drop the key vault collection and create an encryption key. Alternatively, + * this key ID could be read from a configuration file. */ +$client->selectCollection('encryption', '__keyVault')->drop(); +$keyId = $clientEncryption->createDataKey('local'); + +// Select and drop a collection to use for this example +$collection = $client->selectCollection('test', 'coll'); +$collection->drop(); + +// Insert a document with a manually encrypted field +$encryptedValue = $clientEncryption->encrypt('mySecret', [ + 'algorithm' => ClientEncryption::AEAD_AES_256_CBC_HMAC_SHA_512_DETERMINISTIC, + 'keyId' => $keyId, +]); + +$collection->insertOne(['encryptedField' => $encryptedValue]); + +/* Query for the document. The field will not be automatically decrypted + * because the client was not configured with an autoEncryption driver option. + * Manually decrypt the field value using the ClientEncryption object. */ +$document = $collection->findOne(); + +print_r($document->encryptedField); +print_r($clientEncryption->decrypt($document->encryptedField)); diff --git a/docs/examples/csfle-explicit_encryption_automatic_decryption.php b/docs/examples/csfle-explicit_encryption_automatic_decryption.php new file mode 100644 index 000000000..a1cbadf68 --- /dev/null +++ b/docs/examples/csfle-explicit_encryption_automatic_decryption.php @@ -0,0 +1,52 @@ + [ + 'keyVaultNamespace' => 'encryption.__keyVault', + 'kmsProviders' => ['local' => ['key' => $localKey]], + 'bypassAutoEncryption' => true, + ], +]); + +// Create a ClientEncryption object to manage data keys +$clientEncryption = $client->createClientEncryption([ + 'keyVaultNamespace' => 'encryption.__keyVault', + 'kmsProviders' => [ + 'local' => ['key' => $localKey], + ], +]); + +/* Drop the key vault collection and create an encryption key. Alternatively, + * this key ID could be read from a configuration file. */ +$client->selectCollection('encryption', '__keyVault')->drop(); +$keyId = $clientEncryption->createDataKey('local'); + +// Select and drop a collection to use for this example +$collection = $client->selectCollection('test', 'coll'); +$collection->drop(); + +// Insert a document with a manually encrypted field +$encryptedValue = $clientEncryption->encrypt('mySecret', [ + 'algorithm' => ClientEncryption::AEAD_AES_256_CBC_HMAC_SHA_512_DETERMINISTIC, + 'keyId' => $keyId, +]); + +$collection->insertOne(['encryptedField' => $encryptedValue]); + +/* Query for the document. The field will still be automatically decrypted + * because the client was configured with an autoEncryption driver option. */ +$document = $collection->findOne(); + +print_r($document->encryptedField); diff --git a/docs/examples/key_alt_name.php b/docs/examples/key_alt_name.php new file mode 100644 index 000000000..b8dadc292 --- /dev/null +++ b/docs/examples/key_alt_name.php @@ -0,0 +1,37 @@ +createClientEncryption([ + 'keyVaultNamespace' => 'encryption.__keyVault', + 'kmsProviders' => [ + 'local' => ['key' => $localKey], + ], +]); + +/* Drop the key vault collection and create an encryption key with an alternate + * name. This would typically be done during application deployment. To store + * the key ID for later use, you can use serialize() or var_export(). */ +$client->selectCollection('encryption', '__keyVault')->drop(); +$clientEncryption->createDataKey('local', ['keyAltNames' => ['myDataKey']]); + +// Encrypt a value, using the "keyAltName" option instead of "keyId" +$encryptedValue = $clientEncryption->encrypt('mySecret', [ + 'algorithm' => ClientEncryption::AEAD_AES_256_CBC_HMAC_SHA_512_DETERMINISTIC, + 'keyAltName' => 'myDataKey', +]); + +print_r($encryptedValue); diff --git a/docs/examples/queryable_encryption-automatic.php b/docs/examples/queryable_encryption-automatic.php new file mode 100644 index 000000000..40dda5827 --- /dev/null +++ b/docs/examples/queryable_encryption-automatic.php @@ -0,0 +1,79 @@ +createClientEncryption([ + 'keyVaultNamespace' => 'encryption.__keyVault', + 'kmsProviders' => ['local' => ['key' => $localKey]], +]); + +// Drop the key vault collection and create two data keys (one for each encrypted field) +$client->selectCollection('encryption', '__keyVault')->drop(); +$dataKeyId1 = $clientEncryption->createDataKey('local'); +$dataKeyId2 = $clientEncryption->createDataKey('local'); + +/* Create a client with automatic encryption enabled. Define encryptedFields for + * the collection in encryptedFieldsMap. */ +$encryptedClient = new Client($uri, [], [ + 'autoEncryption' => [ + 'keyVaultNamespace' => 'encryption.__keyVault', + 'kmsProviders' => ['local' => ['key' => $localKey]], + 'encryptedFieldsMap' => [ + 'test.coll' => [ + 'fields' => [ + [ + 'path' => 'encryptedIndexed', + 'bsonType' => 'string', + 'keyId' => $dataKeyId1, + 'queries' => ['queryType' => ClientEncryption::QUERY_TYPE_EQUALITY], + ], + [ + 'path' => 'encryptedUnindexed', + 'bsonType' => 'string', + 'keyId' => $dataKeyId2, + ], + ], + ], + ], + ], +]); + +/* Drop and create the collection. Each method will infer encryptedFields from + * the client and manage internal encryption collections automatically. */ +$encryptedClient->selectDatabase('test')->dropCollection('coll'); +$encryptedClient->selectDatabase('test')->createCollection('coll'); +$encryptedCollection = $encryptedClient->selectCollection('test', 'coll'); + +/* Using the encrypted client, insert a document and find it by querying on the + * encrypted field. Fields will be automatically encrypted and decrypted. */ +$indexedValue = 'indexedValue'; +$unindexedValue = 'unindexedValue'; + +$encryptedCollection->insertOne([ + '_id' => 1, + 'encryptedIndexed' => $indexedValue, + 'encryptedUnindexed' => $unindexedValue, +]); + +print_r($encryptedCollection->findOne(['encryptedIndexed' => $indexedValue])); + +/* Using the client configured without encryption, find the same document and + * observe that fields are not automatically decrypted. */ +$unencryptedCollection = $client->selectCollection('test', 'coll'); + +print_r($unencryptedCollection->findOne(['_id' => 1])); diff --git a/docs/examples/queryable_encryption-explicit.php b/docs/examples/queryable_encryption-explicit.php new file mode 100644 index 000000000..a09455ae7 --- /dev/null +++ b/docs/examples/queryable_encryption-explicit.php @@ -0,0 +1,94 @@ +createClientEncryption([ + 'keyVaultNamespace' => 'encryption.__keyVault', + 'kmsProviders' => ['local' => ['key' => $localKey]], +]); + +// Drop the key vault collection and create two data keys (one for each encrypted field) +$client->selectCollection('encryption', '__keyVault')->drop(); +$dataKeyId1 = $clientEncryption->createDataKey('local'); +$dataKeyId2 = $clientEncryption->createDataKey('local'); + +// Create a client with automatic encryption disabled +$encryptedClient = new Client($uri, [], [ + 'autoEncryption' => [ + 'keyVaultNamespace' => 'encryption.__keyVault', + 'kmsProviders' => ['local' => ['key' => $localKey]], + 'bypassQueryAnalysis' => true, + ], +]); + +// Define encryptedFields for the collection +$encryptedFields = [ + 'fields' => [ + [ + 'path' => 'encryptedIndexed', + 'bsonType' => 'string', + 'keyId' => $dataKeyId1, + 'queries' => ['queryType' => ClientEncryption::QUERY_TYPE_EQUALITY], + ], + [ + 'path' => 'encryptedUnindexed', + 'bsonType' => 'string', + 'keyId' => $dataKeyId2, + ], + ], +]; + +/* Drop and create the collection. Pass encryptedFields to each method to ensure + * that internal encryption collections are managed. */ +$encryptedClient->selectDatabase('test')->dropCollection('coll', ['encryptedFields' => $encryptedFields]); +$encryptedClient->selectDatabase('test')->createCollection('coll', ['encryptedFields' => $encryptedFields]); +$collection = $encryptedClient->selectCollection('test', 'coll'); + +// Insert a document with manually encrypted fields +$indexedValue = 'indexedValue'; +$unindexedValue = 'unindexedValue'; + +$insertPayloadIndexed = $clientEncryption->encrypt($indexedValue, [ + 'algorithm' => ClientEncryption::ALGORITHM_INDEXED, + 'contentionFactor' => 1, + 'keyId' => $dataKeyId1, +]); + +$insertPayloadUnindexed = $clientEncryption->encrypt($unindexedValue, [ + 'algorithm' => ClientEncryption::ALGORITHM_UNINDEXED, + 'keyId' => $dataKeyId2, +]); + +$collection->insertOne([ + '_id' => 1, + 'encryptedIndexed' => $insertPayloadIndexed, + 'encryptedUnindexed' => $insertPayloadUnindexed, +]); + +/* Encrypt the payload for an "equality" query using the same key that was used + * to encrypt the insert payload. */ +$findPayload = $clientEncryption->encrypt($indexedValue, [ + 'algorithm' => ClientEncryption::ALGORITHM_INDEXED, + 'queryType' => ClientEncryption::QUERY_TYPE_EQUALITY, + 'contentionFactor' => 1, + 'keyId' => $dataKeyId1, +]); + +/* Find the inserted document. Fields will still be automatically decrypted + * because the client was configured with an autoEncryption driver option. */ +print_r($collection->findOne(['encryptedIndexed' => $findPayload])); diff --git a/docs/tutorial.txt b/docs/tutorial.txt index 91333fb45..bfb44b840 100644 --- a/docs/tutorial.txt +++ b/docs/tutorial.txt @@ -12,7 +12,7 @@ Tutorials /tutorial/commands /tutorial/custom-types /tutorial/decimal128 - /tutorial/client-side-encryption + /tutorial/encryption /tutorial/gridfs /tutorial/indexes /tutorial/tailable-cursor diff --git a/docs/tutorial/client-side-encryption.txt b/docs/tutorial/client-side-encryption.txt deleted file mode 100644 index a8c02699a..000000000 --- a/docs/tutorial/client-side-encryption.txt +++ /dev/null @@ -1,368 +0,0 @@ -====================== -Client-Side Encryption -====================== - -.. default-domain:: mongodb - -.. contents:: On this page - :local: - :backlinks: none - :depth: 1 - :class: singlecol - -Client-Side Field Level Encryption allows administrators and developers to -encrypt specific data fields in addition to other MongoDB encryption features. - - -Creating an Encryption Key --------------------------- - -.. note:: - - The following examples use a local master key; however, other key providers - such as AWS KMS are also an option. This master key is used to encrypt data - keys that are stored locally. It is important that you keep this key secure. - -To create an encryption key, create a :php:`MongoDB\\Driver\\ClientEncryption ` -instance with encryption options and create a new data key. The method will -return the key ID which can be used to reference the key later. You can also -pass multiple alternate names for this key and reference the key by these names -instead of the key ID. Creating a new data encryption key would typically be -done on initial deployment, but depending on your use case you may want to use -more than one encryption key or create them dynamically. - -.. code-block:: php - - '); - - $clientEncryptionOpts = [ - 'keyVaultNamespace' => 'encryption.__keyVault', - 'kmsProviders' => [ - 'local' => ['key' => $localKey], - ], - ]; - - $client = new Client(); - $clientEncryption = $client->createClientEncryption($clientEncryptionOpts); - - // Create an encryption key with an alternate name - // To store the key ID for later use, you can use serialize or var_export - $keyId = $clientEncryption->createDataKey('local', ['keyAltNames' => ['my-encryption-key']]); - -.. seealso:: :manual:`Encryption Key Management ` in the MongoDB manual - - -Automatic Encryption and Decryption ------------------------------------ - -.. note:: - - Auto encryption is an enterprise only feature. - -The following example sets up a collection with automatic encryption based on a -``$jsonSchema`` validator. The data in the ``encryptedField`` field is -automatically encrypted on insertion and decrypted when reading on the client -side. - -.. code-block:: php - - '); - $encryptionOpts = [ - 'keyVaultNamespace' => 'encryption.__keyVault', - 'kmsProviders' => [ - 'local' => ['key' => $localKey], - ], - ]; - - $client = new Client(); - - $database = $client->selectDatabase('test'); - $database->dropCollection('coll'); // remove old data - - // This uses the key ID from the first example. The key ID could be read from - // a configuration file. - $keyId = readDataKey(); - - $database->createCollection('coll', [ - 'validator' => [ - '$jsonSchema' => [ - 'bsonType' => 'object', - 'properties' => [ - 'encryptedField' => [ - 'encrypt' => [ - 'keyId' => [$keyId], - 'bsonType' => 'string', - 'algorithm' => ClientEncryption::AEAD_AES_256_CBC_HMAC_SHA_512_DETERMINISTIC, - ], - ], - ], - ], - ], - ]); - - $encryptedClient = new Client('mongodb://127.0.0.1', [], ['autoEncryption' => $encryptionOpts]); - - $collection = $encryptedClient->selectCollection('test', 'coll'); - - $collection->insertOne(['encryptedField' => '123456789']); - - var_dump($collection->findOne([])); - - -Specifying an Explicit Schema for Encryption --------------------------------------------- - -The following example uses the ``schemaMap`` encryption option to define -encrypted fields. - -.. note:: - - Supplying a ``schemaMap`` provides more security than relying on JSON schemas - obtained from the server. It protects against a malicious server advertising - a false JSON schema, which could trick the client into sending unencrypted - data that should be encrypted. - -.. code-block:: php - - '); - - $client = new Client(); - - // This uses the key ID from the first example. The key ID could be read from - // a configuration file. - $keyId = readDataKey(); - - $autoEncryptionOpts = [ - 'keyVaultNamespace' => 'encryption.__keyVault', - 'kmsProviders' => [ - 'local' => ['key' => $localKey], - ], - 'schemaMap' => [ - 'test.coll' => [ - 'bsonType' => 'object', - 'properties' => [ - 'encryptedField' => [ - 'encrypt' => [ - 'keyId' => [$keyId], - 'bsonType' => 'string', - 'algorithm' => ClientEncryption::AEAD_AES_256_CBC_HMAC_SHA_512_DETERMINISTIC, - ], - ], - ], - ], - ], - ]; - - $encryptedClient = new Client(null, [], ['autoEncryption' => $autoEncryptionOpts]); - - $collection = $encryptedClient->selectCollection('test', 'coll'); - $collection->drop(); // clear old data - - $collection->insertOne(['encryptedField' => '123456789']); - - var_dump($collection->findOne([])); - - -Manually Encrypting and Decrypting Values ------------------------------------------ - -In the MongoDB Community Edition, you will have to manually encrypt values -before storing them in the database. The following example assumes that you have -already created an encryption key in the key vault collection and explicitly -encrypts and decrypts values in the document. - -.. code-block:: php - - '); - - $clientEncryptionOpts = [ - 'keyVaultNamespace' => 'encryption.__keyVault', - 'kmsProviders' => [ - 'local' => ['key' => $localKey], - ], - ]; - - $client = new Client(); - $clientEncryption = $client->createClientEncryption($clientEncryptionOpts); - - // This uses the key ID from the first example. The key ID could be read from - // a configuration file. - $keyId = readDataKey(); - - $collection = $client->selectCollection('test', 'coll'); - $collection->drop(); // clear old data - - $encryptionOpts = [ - 'keyId' => $keyId, - 'algorithm' => ClientEncryption::AEAD_AES_256_CBC_HMAC_SHA_512_DETERMINISTIC, - ]; - $encryptedValue = $clientEncryption->encrypt('123456789', $encryptionOpts); - - $collection->insertOne(['encryptedField' => $encryptedValue]); - - $document = $collection->findOne(); - var_dump($clientEncryption->decrypt($document->encryptedField)); - - -Referencing Encryption Keys by an Alternative Name --------------------------------------------------- - -While it is possible to create an encryption key every time data is encrypted, -this is not the recommended approach. Instead, you should create your encryption -keys depending on your use case, e.g. by creating a user-specific encryption -key. To reference keys in your software, you can use the keyAltName attribute -specified when creating the key. The following example creates an encryption key -with an alternative name, which could be done when deploying the application. -The software then encrypts data by referencing the key by its alternative name. - -To use an alternate name when referencing an encryption key, use the -``keyAltName`` option instead of ``keyId``. - -.. code-block:: php - - '); - - $clientEncryptionOpts = [ - 'keyVaultNamespace' => 'encryption.__keyVault', - 'kmsProviders' => [ - 'local' => ['key' => $localKey], - ], - ]; - - $client = new Client(); - $clientEncryption = $client->createClientEncryption($clientEncryptionOpts); - - $collection = $client->selectCollection('test', 'coll'); - $collection->drop(); // clear old data - - // Reference the encryption key created in the first example by its - // alternative name - $encryptionOpts = [ - 'keyAltName' => 'my-encryption-key', - 'algorithm' => ClientEncryption::AEAD_AES_256_CBC_HMAC_SHA_512_DETERMINISTIC, - ]; - $encryptedValue = $clientEncryption->encrypt('123456789', $encryptionOpts); - - $collection->insertOne(['encryptedField' => $encryptedValue]); - - $document = $collection->findOne(); - var_dump($clientEncryption->decrypt($document->encryptedField)); - - -Automatic Queryable Encryption ------------------------------- - -.. note:: - - Automatic queryable encryption is an enterprise only feature and requires - MongoDB 7.0+. - -The following example uses a local key; however, other key providers such as AWS -are also an option. The data in the ``encryptedIndexed`` and -``encryptedUnindexed`` fields will be automatically encrypted on insertion and -decrypted when querying on the client side. Additionally, it is possible to -query on the ``encryptedIndexed`` field. - -.. code-block:: php - - '); - - $encryptionOpts = [ - 'keyVaultNamespace' => 'encryption.__keyVault', - 'kmsProviders' => ['local' => ['key' => $localKey]], - ]; - - $client = new Client(); - $clientEncryption = $client->createClientEncryption($encryptionOpts); - - // Create two data keys, one for each encrypted field - $dataKeyId1 = $clientEncryption->createDataKey('local'); - $dataKeyId2 = $clientEncryption->createDataKey('local'); - - $autoEncryptionOpts = [ - 'keyVaultNamespace' => 'encryption.__keyVault', - 'kmsProviders' => ['local' => ['key' => $localKey]], - 'encryptedFieldsMap' => [ - 'test.coll' => [ - 'fields' => [ - [ - 'path' => 'encryptedIndexed', - 'bsonType' => 'string', - 'keyId' => $dataKeyId1, - 'queries' => ['queryType' => 'equality'], - ], - [ - 'path' => 'encryptedUnindexed', - 'bsonType' => 'string', - 'keyId' => $dataKeyId2, - ], - ], - ], - ], - ]; - - $encryptedClient = new Client(null, [], ['autoEncryption' => $autoEncryptionOpts]); - - /* Drop and create the collection under test. The createCollection() helper - * will reference the client's encryptedFieldsMap and create additional, - * internal collections automatically. */ - $encryptedClient->selectDatabase('test')->dropCollection('coll'); - $encryptedClient->selectDatabase('test')->createCollection('coll'); - $encryptedCollection = $encryptedClient->selectCollection('test', 'coll'); - - /* Using a client with auto encryption, insert a document with encrypted - * fields and assert that those fields are automatically decrypted when - * querying. The encryptedIndexed and encryptedUnindexed fields should both - * be strings. */ - $indexedValue = 'indexedValue'; - $unindexedValue = 'unindexedValue'; - - $encryptedCollection->insertOne([ - '_id' => 1, - 'encryptedIndexed' => $indexedValue, - 'encryptedUnindexed' => $unindexedValue, - ]); - - var_dump($encryptedCollection->findOne(['encryptedIndexed' => $indexedValue])); - - /* Using a client without auto encryption, query for the same document and - * assert that encrypted data is returned. The encryptedIndexed and - * encryptedUnindexed fields should both be Binary objects. */ - $unencryptedCollection = $client->selectCollection('test', 'coll'); - - var_dump($unencryptedCollection->findOne(['_id' => 1])); diff --git a/docs/tutorial/encryption.txt b/docs/tutorial/encryption.txt new file mode 100644 index 000000000..3e7af07bd --- /dev/null +++ b/docs/tutorial/encryption.txt @@ -0,0 +1,265 @@ +================= +In-Use Encryption +================= + +.. default-domain:: mongodb + +.. contents:: On this page + :local: + :backlinks: none + :depth: 3 + :class: singlecol + + +Dependencies +------------ + +To get started using in-use encryption in your project, the +`PHP driver `_ (i.e. ``mongodb`` extension) will need +to be compiled with `libmongocrypt `_ +(enabled by default). + +Additionally, either `crypt_shared`_ or `mongocryptd`_ are required in order to +use *automatic* client-side encryption. Neither is required for *explicit* +encryption. + + +crypt_shared +~~~~~~~~~~~~ + +The :manual:`Automatic Encryption Shared Library ` +(crypt_shared) provides the same functionality as mongocryptd_, but does not +require you to spawn another process to perform automatic encryption. + +By default, the PHP driver attempts to load crypt_shared from the system path(s) +and uses it automatically if found. To load crypt_shared from another location, +use the ``cryptSharedLibPath`` auto encryption +:php:`driver option ` +when constructing a client. If the driver cannot load crypt_shared it will +attempt to fallback to using mongocryptd by default. The +``cryptSharedLibRequired`` option may be used to always require crypt_shared and +fail if it cannot be loaded. + +For detailed installation instructions see the MongoDB documentation for the +:manual:`Automatic Encryption Shared Library `. + + +mongocryptd +~~~~~~~~~~~ + +The mongocryptd binary is an alternative requirement for automatic client-side +encryption and is included as a component in the +:manual:`MongoDB Enterprise Server package `. +For detailed installation instructions see the +:manual:`MongoDB documentation on mongocryptd `. + +mongocryptd performs the following: + +- Parses the automatic encryption rules specified in the client configuration. + If the ``schemaMap`` auto encryption driver option contains invalid syntax, + mongocryptd returns an error. + +- Uses the specified automatic encryption rules to mark fields in read and write + operations for encryption. + +- Rejects read/write operations that may return unexpected or incorrect results + when applied to an encrypted field. For supported and unsupported operations, + see :manual:`Supported Operations for Automatic Encryption `. + +A client configured with auto encryption will automatically spawn the +mongocryptd process from the application's ``PATH``. Applications can control +the spawning behavior via various auto encryption +:php:`driver options `. + +mongocryptd is only responsible for supporting automatic client-side encryption +and does not itself perform any encryption or decryption. + + +Managing Encryption Keys +------------------------ + +.. seealso:: :manual:`Encryption Key Management ` in the MongoDB manual + +Creating an Encryption Key +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. note:: + + The following examples use a local master key. While this is suitable for + development, a production application should use a supported cloud provider + (e.g. AWS KMS). The master key is used to encrypt locally stored data keys + and thus it is very important that you keep this key secure. + +To create an encryption key, create a +:php:`MongoDB\\Driver\\ClientEncryption ` +instance with encryption options and create a new data key. The method will +return the key ID which can be used to reference the key later. You can also +pass multiple alternate names for this key and reference the key by these names +instead of the key ID. Creating a new data encryption key would typically be +done on initial deployment, but depending on your use case you may want to use +more than one encryption key or create them dynamically. + +.. literalinclude:: /examples/create_data_key.php + :language: php + + +Referencing Encryption Keys by an Alternative Name +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +While it is possible to create an encryption key every time data is encrypted, +this is not the recommended approach. Instead, you should create your encryption +keys depending on your use case, e.g. by creating a user-specific encryption +key. To reference keys in your software, you can use the ``keyAltName`` +attribute specified when creating the key. The following example creates an +encryption key with an alternative name, which could be done when deploying the +application. The script then encrypts data by referencing the key by its +alternative name. + +To use an alternate name when referencing an encryption key, use the +``keyAltName`` option instead of ``keyId``. + +.. literalinclude:: /examples/key_alt_name.php + :language: php + + +Client-Side Field Level Encryption +---------------------------------- + +Introduced in MongoDB 4.2, +:manual:`Client-Side Field Level Encryption ` allows an +application to encrypt specific data fields in addition to pre-existing MongoDB +encryption features such as +:manual:`Encryption at Rest ` and +:manual:`TLS/SSL (Transport Encryption) `. + +With field level encryption, applications can encrypt fields in documents prior +to transmitting data over the wire to the server. Client-side field level +encryption supports workloads where applications must guarantee that +unauthorized parties, including server administrators, cannot read the encrypted +data. + + +Automatic Client-Side Field Level Encryption +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. note:: + + Automatic client-side field level encryption requires MongoDB 4.2+ Enterprise + or a MongoDB 4.2+ Atlas cluster. + +Automatic client-side field level encryption is enabled by creating a client and +specifying the ``autoEncryption`` +:php:`driver option `. +The following examples demonstrate how to setup automatic client-side field +level encryption and use a +:php:`MongoDB\\Driver\\ClientEncryption ` +object to create a new encryption key. + + +Providing Local Automatic Encryption Rules +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The following example uses the ``schemaMap`` auto encryption driver option to +define encrypted fields using a +:manual:`strict subset of the JSON schema syntax `. + +Specifying a ``schemaMap`` this way provides more security than relying on JSON +schemas obtained from the server. It protects against a malicious server +advertising a false JSON schema, which could trick the client into sending +unencrypted data that should be encrypted. + +JSON schemas specified in the ``schemaMap`` auto encryption driver option only +apply to configuring automatic client-side field level encryption. Other +validation rules in the JSON schema will not be enforced by the driver and will +result in an error. + +.. literalinclude:: /examples/csfle-automatic_encryption-local_schema.php + :language: php + + +Server-Side Field Level Encryption Enforcement +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The MongoDB 4.2+ server supports using schema validation to enforce encryption +of specific fields in a collection. This schema validation will prevent an +application from inserting unencrypted values for any fields marked with the +:manual:`"encrypt" JSON schema keyword `. + +The following example sets up a collection with automatic encryption using a +``$jsonSchema`` validator and +:manual:`Encryption Schema syntax `. +Data in the ``encryptedField`` field is automatically encrypted on insertion and +decrypted when reading on the client side. + +.. literalinclude:: /examples/csfle-automatic_encryption-server_side_schema.php + :language: php + + +Explicit Encryption +~~~~~~~~~~~~~~~~~~~ + +Explicit encryption is a MongoDB community feature and does not use +crypt_shared_ or mongocryptd_. Explicit encryption is provided by the +:php:`MongoDB\\Driver\\ClientEncryption ` class. + +.. literalinclude:: /examples/csfle-explicit_encryption.php + :language: php + + +Explicit Encryption with Automatic Decryption +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Although automatic encryption requires MongoDB 4.2+ enterprise or a MongoDB 4.2+ +Atlas cluster, automatic *decryption* is supported for all users. To configure +automatic decryption without automatic encryption set the +``bypassAutoEncryption`` auto encryption +:php:`driver option ` +when constructing a client. + +.. literalinclude:: /examples/csfle-explicit_encryption_automatic_decryption.php + :language: php + + +Queryable Encryption +-------------------- + +Introduced in MongoDB 7.0, +:manual:`Queryable Encryption ` is another +form of in-use encryption. Data is encrypted client-side. Queryable Encryption +supports indexed encrypted fields, which are further processed server-side. + + +Automatic Queryable Encryption +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. note:: + + Automatic queryable encryption requires MongoDB 7.0+ Enterprise or a MongoDB + 7.0+ Atlas cluster. + +Automatic encryption in Queryable Encryption utilizes ``crypt_shared`` or +``mongocryptd`` to automatically encrypt and decrypt data client-side. The data +in the ``encryptedIndexed`` and ``encryptedUnindexed`` fields will be +automatically encrypted on insertion and decrypted when querying on the client +side. Additionally, it is possible to query on the ``encryptedIndexed`` field. + +.. literalinclude:: /examples/queryable_encryption-automatic.php + :language: php + + +Explicit Queryable Encryption +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. note:: + + Explicit queryable encryption requires MongoDB 7.0+. + +Explicit encryption in Queryable Encryption is performed using the +:php:`MongoDB\Driver\ClientEncryption::encrypt() ` +and :php:`decrypt() ` methods. Although +values must be explicitly encrypted (e.g. insertions, query criteria), automatic +decryption for queries is possible by configuring ``encryptedFields`` on the +collection, as demonstrated in the following example: + +.. literalinclude:: /examples/queryable_encryption-explicit.php + :language: php diff --git a/phpcs.xml.dist b/phpcs.xml.dist index a4dee252f..b474f2a4e 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -10,6 +10,7 @@ src + docs/examples examples tests tools diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 22cc4833f..b761a397f 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,63 @@ + + + new Binary(random_bytes(96)) + + + + + new Binary(random_bytes(96)) + + + + + new Binary(random_bytes(96)) + + + + + $document->encryptedField + + + $document->encryptedField + + + $document->encryptedField + + + $document->encryptedField + + + new Binary(random_bytes(96)) + + + + + $document->encryptedField + + + $document->encryptedField + + + new Binary(random_bytes(96)) + + + + + new Binary(random_bytes(96)) + + + + + new Binary(random_bytes(96)) + + + + + new Binary(random_bytes(96)) + + $address diff --git a/psalm.xml.dist b/psalm.xml.dist index 5a922b4ca..0acafb77f 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -9,6 +9,7 @@ > + diff --git a/tests/ExamplesTest.php b/tests/ExamplesTest.php index 997bc3312..3c9002385 100644 --- a/tests/ExamplesTest.php +++ b/tests/ExamplesTest.php @@ -16,13 +16,19 @@ public function setUp(): void } if ($this->isApiVersionRequired()) { - $this->markTestSkipped('Examples are not tested with when the server requires specifying an API version.'); + $this->markTestSkipped('Examples are not tested when the server requires specifying an API version.'); } self::createTestClient()->dropDatabase('test'); } - public function dataExamples(): Generator + /** @dataProvider provideExamples */ + public function testExamples(string $file, string $expectedOutput): void + { + $this->assertExampleOutput($file, $expectedOutput); + } + + public static function provideExamples(): Generator { $expectedOutput = <<<'OUTPUT' { "_id" : null, "totalCount" : 100, "evenCount" : %d, "oddCount" : %d, "maxValue" : %d, "minValue" : %d } @@ -174,15 +180,7 @@ public function testChangeStream(): void Aborting after 3 seconds... OUTPUT; - $this->testExample(__DIR__ . '/../examples/changestream.php', $expectedOutput); - } - - /** @dataProvider dataExamples */ - public function testExample(string $file, string $expectedOutput): void - { - require $file; - - self::assertStringMatchesFormat($expectedOutput, $this->getActualOutputForAssertion()); + $this->assertExampleOutput(__DIR__ . '/../examples/changestream.php', $expectedOutput); } public function testWithTransaction(): void @@ -195,6 +193,259 @@ public function testWithTransaction(): void %s OUTPUT; - $this->testExample(__DIR__ . '/../examples/with_transaction.php', $expectedOutput); + $this->assertExampleOutput(__DIR__ . '/../examples/with_transaction.php', $expectedOutput); + } + + /** @dataProvider provideEncryptionExamples */ + public function testEncryptionExamples(string $file, string $expectedOutput): void + { + $this->skipIfClientSideEncryptionIsNotSupported(); + + $this->assertExampleOutput($file, $expectedOutput); + + // Clean up metadata and key vault collections + $this->dropCollection('test', 'coll', ['encryptedFields' => []]); + $this->dropCollection('encryption', '__keyVault'); + } + + public static function provideEncryptionExamples(): Generator + { + $expectedOutput = <<<'OUTPUT' +MongoDB\BSON\Binary Object +( + [data] => %a + [type] => 4 +) +MongoDB\BSON\Binary Object +( + [data] => %a + [type] => 6 +) +OUTPUT; + + yield 'create_data_key' => [ + 'file' => __DIR__ . '/../docs/examples/create_data_key.php', + 'expectedOutput' => $expectedOutput, + ]; + + $expectedOutput = <<<'OUTPUT' +MongoDB\BSON\Binary Object +( + [data] => %a + [type] => 6 +) +OUTPUT; + + yield 'key_alt_name' => [ + 'file' => __DIR__ . '/../docs/examples/key_alt_name.php', + 'expectedOutput' => $expectedOutput, + ]; + + $expectedOutput = <<<'OUTPUT' +MongoDB\Model\BSONDocument Object +( + [storage:ArrayObject:private] => Array + ( + [_id] => 1 + [encryptedField] => mySecret + ) + +) +MongoDB\Model\BSONDocument Object +( + [storage:ArrayObject:private] => Array + ( + [_id] => 1 + [encryptedField] => MongoDB\BSON\Binary Object + ( + [data] => %a + [type] => 6 + ) + + ) + +) +OUTPUT; + + yield 'csfle-automatic_encryption-local_schema' => [ + 'file' => __DIR__ . '/../docs/examples/csfle-automatic_encryption-local_schema.php', + 'expectedOutput' => $expectedOutput, + ]; + + $expectedOutput = <<<'OUTPUT' +MongoDB\Model\BSONDocument Object +( + [storage:ArrayObject:private] => Array + ( + [_id] => 1 + [encryptedField] => mySecret + ) + +) +MongoDB\Model\BSONDocument Object +( + [storage:ArrayObject:private] => Array + ( + [_id] => 1 + [encryptedField] => MongoDB\BSON\Binary Object + ( + [data] => %a + [type] => 6 + ) + + ) + +) +OUTPUT; + + yield 'csfle-automatic_encryption-server_side_schema' => [ + 'file' => __DIR__ . '/../docs/examples/csfle-automatic_encryption-server_side_schema.php', + 'expectedOutput' => $expectedOutput, + ]; + + $expectedOutput = <<<'OUTPUT' +MongoDB\BSON\Binary Object +( + [data] => %a + [type] => 6 +) +mySecret +OUTPUT; + + yield 'csfle-explicit_encryption' => [ + 'file' => __DIR__ . '/../docs/examples/csfle-explicit_encryption.php', + 'expectedOutput' => $expectedOutput, + ]; + + $expectedOutput = <<<'OUTPUT' +mySecret +OUTPUT; + + yield 'csfle-explicit_encryption_automatic_decryption' => [ + 'file' => __DIR__ . '/../docs/examples/csfle-explicit_encryption_automatic_decryption.php', + 'expectedOutput' => $expectedOutput, + ]; + } + + /** @dataProvider provideQueryableEncryptionExamples */ + public function testQueryableEncryptionExamples(string $file, string $expectedOutput): void + { + $this->skipIfClientSideEncryptionIsNotSupported(); + + $this->skipIfServerVersion('<', '7.0.0', 'Queryable encryption tests require MongoDB 7.0 or later'); + + $this->assertExampleOutput($file, $expectedOutput); + + // Clean up metadata and key vault collections + $this->dropCollection('test', 'coll', ['encryptedFields' => []]); + $this->dropCollection('encryption', '__keyVault'); + } + + public static function provideQueryableEncryptionExamples(): Generator + { + $expectedOutput = <<<'OUTPUT' +MongoDB\Model\BSONDocument Object +( + [storage:ArrayObject:private] => Array + ( + [_id] => 1 + [encryptedIndexed] => indexedValue + [encryptedUnindexed] => unindexedValue + [__safeContent__] => MongoDB\Model\BSONArray Object + ( + [storage:ArrayObject:private] => Array + ( + [0] => MongoDB\BSON\Binary Object + ( + [data] => %a + [type] => 0 + ) + + ) + + ) + + ) + +) +MongoDB\Model\BSONDocument Object +( + [storage:ArrayObject:private] => Array + ( + [_id] => 1 + [encryptedIndexed] => MongoDB\BSON\Binary Object + ( + [data] => %a + [type] => 6 + ) + + [encryptedUnindexed] => MongoDB\BSON\Binary Object + ( + [data] => %a + [type] => 6 + ) + + [__safeContent__] => MongoDB\Model\BSONArray Object + ( + [storage:ArrayObject:private] => Array + ( + [0] => MongoDB\BSON\Binary Object + ( + [data] => %a + [type] => 0 + ) + + ) + + ) + + ) + +) +OUTPUT; + + yield 'queryable_encryption-automatic' => [ + 'file' => __DIR__ . '/../docs/examples/queryable_encryption-automatic.php', + 'expectedOutput' => $expectedOutput, + ]; + + $expectedOutput = <<<'OUTPUT' +MongoDB\Model\BSONDocument Object +( + [storage:ArrayObject:private] => Array + ( + [_id] => 1 + [encryptedIndexed] => indexedValue + [encryptedUnindexed] => unindexedValue + [__safeContent__] => MongoDB\Model\BSONArray Object + ( + [storage:ArrayObject:private] => Array + ( + [0] => MongoDB\BSON\Binary Object + ( + [data] => %a + [type] => 0 + ) + + ) + + ) + + ) + +) +OUTPUT; + + yield 'queryable_encryption-explicit' => [ + 'file' => __DIR__ . '/../docs/examples/queryable_encryption-explicit.php', + 'expectedOutput' => $expectedOutput, + ]; + } + + private function assertExampleOutput(string $file, string $expectedOutput): void + { + require $file; + + $this->assertStringMatchesFormat($expectedOutput, $this->getActualOutputForAssertion()); } } From c2ca7b07171aa024aabe23e0dca633f9a115cffe Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Sun, 9 Jul 2023 21:27:48 -0400 Subject: [PATCH 2/6] Code review feedback Demonstrates schema validation errors in example scripts Relocates local schema section after server-side schema, and clarifies that it should be used in conjunction with server-side schemas (not instead of). Revise comments and variable names in scripts and update expected output for tests accordingly. --- docs/examples/create_data_key.php | 13 ++- ...sfle-automatic_encryption-local_schema.php | 74 +++++++----- ...utomatic_encryption-server_side_schema.php | 57 +++++----- docs/examples/csfle-explicit_encryption.php | 24 ++-- ...plicit_encryption_automatic_decryption.php | 17 +-- docs/examples/key_alt_name.php | 21 +++- .../queryable_encryption-automatic.php | 38 +++---- .../queryable_encryption-explicit.php | 59 +++++----- docs/tutorial/encryption.txt | 70 ++++++------ tests/ExamplesTest.php | 106 +++++++++--------- 10 files changed, 260 insertions(+), 219 deletions(-) diff --git a/docs/examples/create_data_key.php b/docs/examples/create_data_key.php index 9de810d07..4fdec0664 100644 --- a/docs/examples/create_data_key.php +++ b/docs/examples/create_data_key.php @@ -11,10 +11,10 @@ // Generate a secure local key to use for this script $localKey = new Binary(random_bytes(96)); -/* Create a client with no encryption options. Additionally, create a - * ClientEncryption object to manage data keys. */ +// Create a client with no encryption options $client = new Client($uri); +// Create a ClientEncryption object to manage data encryption keys $clientEncryption = $client->createClientEncryption([ 'keyVaultNamespace' => 'encryption.__keyVault', 'kmsProviders' => [ @@ -22,10 +22,13 @@ ], ]); -/* Drop the key vault collection and create an encryption key. This would - * typically be done during application deployment. To store the key ID for - * later use, you can use serialize() or var_export(). */ +/* Create a new key vault collection for this script. The application must also + * ensure that a unique index exists for keyAltNames. */ $client->selectCollection('encryption', '__keyVault')->drop(); +$client->selectCollection('encryption', '__keyVault')->createIndex(['keyAltNames' => 1], ['unique' => true]); + +/* Create a data encryption key. To store the key ID for later use, you can use + * serialize(), var_export(), etc. */ $keyId = $clientEncryption->createDataKey('local'); print_r($keyId); diff --git a/docs/examples/csfle-automatic_encryption-local_schema.php b/docs/examples/csfle-automatic_encryption-local_schema.php index d61be162e..bfa8e0456 100644 --- a/docs/examples/csfle-automatic_encryption-local_schema.php +++ b/docs/examples/csfle-automatic_encryption-local_schema.php @@ -3,6 +3,7 @@ use MongoDB\BSON\Binary; use MongoDB\Client; use MongoDB\Driver\ClientEncryption; +use MongoDB\Driver\Exception\ServerException; require __DIR__ . '/../../vendor/autoload.php'; @@ -11,10 +12,10 @@ // Generate a secure local key to use for this script $localKey = new Binary(random_bytes(96)); -/* Create a client with no encryption options. Additionally, create a - * ClientEncryption object to manage data keys. */ +// Create a client with no encryption options $client = new Client($uri); +// Create a ClientEncryption object to manage data encryption keys $clientEncryption = $client->createClientEncryption([ 'keyVaultNamespace' => 'encryption.__keyVault', 'kmsProviders' => [ @@ -22,51 +23,64 @@ ], ]); -/* Drop the key vault collection and create an encryption key. Alternatively, - * this key ID could be read from a configuration file. */ +/* Create a new key vault collection and data encryption key for this script. + * Alternatively, this key ID could be read from a configuration file. */ $client->selectCollection('encryption', '__keyVault')->drop(); +$client->selectCollection('encryption', '__keyVault')->createIndex(['keyAltNames' => 1], ['unique' => true]); $keyId = $clientEncryption->createDataKey('local'); -/* Create a client with automatic encryption enabled. Specify a schemaMap option - * to enforce a local JSON schema. */ +/* Define a JSON schema for the encrypted collection. Since this only utilizes + * encryption schema syntax, it can be used for both the server-side and local + * schema. */ +$schema = [ + 'bsonType' => 'object', + 'properties' => [ + 'encryptedField' => [ + 'encrypt' => [ + 'keyId' => [$keyId], + 'bsonType' => 'string', + 'algorithm' => ClientEncryption::AEAD_AES_256_CBC_HMAC_SHA_512_DETERMINISTIC, + ], + ], + ], +]; + +/* Create another client with automatic encryption enabled. Configure a local + * schema for the encrypted collection using the "schemaMap" option. */ $encryptedClient = new Client($uri, [], [ 'autoEncryption' => [ 'keyVaultNamespace' => 'encryption.__keyVault', 'kmsProviders' => ['local' => ['key' => $localKey]], - 'schemaMap' => [ - 'test.coll' => [ - 'bsonType' => 'object', - 'properties' => [ - 'encryptedField' => [ - 'encrypt' => [ - 'keyId' => [$keyId], - 'bsonType' => 'string', - 'algorithm' => ClientEncryption::AEAD_AES_256_CBC_HMAC_SHA_512_DETERMINISTIC, - ], - ], - ], - ], - ], + 'schemaMap' => ['test.coll' => $schema], ], ]); -// Drop and create the collection. +/* Create a new collection for this script. Configure a server-side schema by + * explicitly creating the collection with a "validator" option. + * + * Note: without a server-side schema, another client could potentially insert + * unencrypted data into the collection. Therefore, a local schema should always + * be used in conjunction with a server-side schema. */ $encryptedClient->selectDatabase('test')->dropCollection('coll'); -$encryptedClient->selectDatabase('test')->createCollection('coll'); +$encryptedClient->selectDatabase('test')->createCollection('coll', ['validator' => ['$jsonSchema' => $schema]]); $encryptedCollection = $encryptedClient->selectCollection('test', 'coll'); -/* Using the encrypted client, insert and find a document. The encrypted field - * will be automatically encrypted and decrypted. */ -$encryptedCollection->insertOne([ - '_id' => 1, - 'encryptedField' => 'mySecret', -]); +/* Using the encrypted client, insert and find a document to demonstrate that + * the encrypted field is automatically encrypted and decrypted. */ +$encryptedCollection->insertOne(['_id' => 1, 'encryptedField' => 'mySecret']); print_r($encryptedCollection->findOne(['_id' => 1])); /* Using the client configured without encryption, find the same document and - * observe that the field is not automatically decrypted. Additionally, the JSON - * schema will prohibit inserting a document with an unencrypted field value. */ + * observe that the field is not automatically decrypted. */ $unencryptedCollection = $client->selectCollection('test', 'coll'); print_r($unencryptedCollection->findOne(['_id' => 1])); + +/* Attempt to insert another document with an unencrypted field value to + * demonstrate that the server-side schema is enforced. */ +try { + $unencryptedCollection->insertOne(['_id' => 2, 'encryptedField' => 'myOtherSecret']); +} catch (ServerException $e) { + printf("Error inserting document: %s\n", $e->getMessage()); +} diff --git a/docs/examples/csfle-automatic_encryption-server_side_schema.php b/docs/examples/csfle-automatic_encryption-server_side_schema.php index 0474da7a2..e47314a0e 100644 --- a/docs/examples/csfle-automatic_encryption-server_side_schema.php +++ b/docs/examples/csfle-automatic_encryption-server_side_schema.php @@ -3,6 +3,7 @@ use MongoDB\BSON\Binary; use MongoDB\Client; use MongoDB\Driver\ClientEncryption; +use MongoDB\Driver\Exception\ServerException; require __DIR__ . '/../../vendor/autoload.php'; @@ -11,10 +12,10 @@ // Generate a secure local key to use for this script $localKey = new Binary(random_bytes(96)); -/* Create a client with no encryption options. Additionally, create a - * ClientEncryption object to manage data keys. */ +// Create a client with no encryption options $client = new Client($uri); +// Create a ClientEncryption object to manage data encryption keys $clientEncryption = $client->createClientEncryption([ 'keyVaultNamespace' => 'encryption.__keyVault', 'kmsProviders' => [ @@ -22,12 +23,13 @@ ], ]); -/* Drop the key vault collection and create an encryption key. Alternatively, - * this key ID could be read from a configuration file. */ +/* Create a new key vault collection and data encryption key for this script. + * Alternatively, this key ID could be read from a configuration file. */ $client->selectCollection('encryption', '__keyVault')->drop(); +$client->selectCollection('encryption', '__keyVault')->createIndex(['keyAltNames' => 1], ['unique' => true]); $keyId = $clientEncryption->createDataKey('local'); -// Create a client with automatic encryption enabled +// Create another client with automatic encryption enabled $encryptedClient = new Client($uri, [], [ 'autoEncryption' => [ 'keyVaultNamespace' => 'encryption.__keyVault', @@ -35,39 +37,42 @@ ], ]); -/* Drop and create the collection. Specify a validator option when creating the - * collection to enforce a server-side JSON schema. */ -$validator = [ - '$jsonSchema' => [ - 'bsonType' => 'object', - 'properties' => [ - 'encryptedField' => [ - 'encrypt' => [ - 'keyId' => [$keyId], - 'bsonType' => 'string', - 'algorithm' => ClientEncryption::AEAD_AES_256_CBC_HMAC_SHA_512_DETERMINISTIC, - ], +// Define a JSON schema for the encrypted collection +$schema = [ + 'bsonType' => 'object', + 'properties' => [ + 'encryptedField' => [ + 'encrypt' => [ + 'keyId' => [$keyId], + 'bsonType' => 'string', + 'algorithm' => ClientEncryption::AEAD_AES_256_CBC_HMAC_SHA_512_DETERMINISTIC, ], ], ], ]; +/* Create a new collection for this script. Configure a server-side schema by + * explicitly creating the collection with a "validator" option. */ $encryptedClient->selectDatabase('test')->dropCollection('coll'); -$encryptedClient->selectDatabase('test')->createCollection('coll', ['validator' => $validator]); +$encryptedClient->selectDatabase('test')->createCollection('coll', ['validator' => ['$jsonSchema' => $schema]]); $encryptedCollection = $encryptedClient->selectCollection('test', 'coll'); -/* Using the encrypted client, insert and find a document. The encrypted field - * will be automatically encrypted and decrypted. */ -$encryptedCollection->insertOne([ - '_id' => 1, - 'encryptedField' => 'mySecret', -]); +/* Using the encrypted client, insert and find a document to demonstrate that + * the encrypted field is automatically encrypted and decrypted. */ +$encryptedCollection->insertOne(['_id' => 1, 'encryptedField' => 'mySecret']); print_r($encryptedCollection->findOne(['_id' => 1])); /* Using the client configured without encryption, find the same document and - * observe that the field is not automatically decrypted. Additionally, the JSON - * schema will prohibit inserting a document with an unencrypted field value. */ + * observe that the field is not automatically decrypted. */ $unencryptedCollection = $client->selectCollection('test', 'coll'); print_r($unencryptedCollection->findOne(['_id' => 1])); + +/* Attempt to insert another document with an unencrypted field value to + * demonstrate that the server-side schema is enforced. */ +try { + $unencryptedCollection->insertOne(['_id' => 2, 'encryptedField' => 'myOtherSecret']); +} catch (ServerException $e) { + printf("Error inserting document: %s\n", $e->getMessage()); +} diff --git a/docs/examples/csfle-explicit_encryption.php b/docs/examples/csfle-explicit_encryption.php index 34fc5b81c..ac2d0a59d 100644 --- a/docs/examples/csfle-explicit_encryption.php +++ b/docs/examples/csfle-explicit_encryption.php @@ -11,10 +11,10 @@ // Generate a secure local key to use for this script $localKey = new Binary(random_bytes(96)); -/* Create a client with no encryption options. Additionally, create a - * ClientEncryption object to manage data keys. */ +// Create a client with no encryption options $client = new Client($uri); +// Create a ClientEncryption object to manage data encryption keys $clientEncryption = $client->createClientEncryption([ 'keyVaultNamespace' => 'encryption.__keyVault', 'kmsProviders' => [ @@ -22,12 +22,13 @@ ], ]); -/* Drop the key vault collection and create an encryption key. Alternatively, - * this key ID could be read from a configuration file. */ +/* Create a new key vault collection and data encryption key for this script. + * Alternatively, this key ID could be read from a configuration file. */ $client->selectCollection('encryption', '__keyVault')->drop(); +$client->selectCollection('encryption', '__keyVault')->createIndex(['keyAltNames' => 1], ['unique' => true]); $keyId = $clientEncryption->createDataKey('local'); -// Select and drop a collection to use for this example +// Create a new collection for this script $collection = $client->selectCollection('test', 'coll'); $collection->drop(); @@ -37,12 +38,13 @@ 'keyId' => $keyId, ]); -$collection->insertOne(['encryptedField' => $encryptedValue]); +$collection->insertOne(['_id' => 1, 'encryptedField' => $encryptedValue]); -/* Query for the document. The field will not be automatically decrypted - * because the client was not configured with an autoEncryption driver option. - * Manually decrypt the field value using the ClientEncryption object. */ +/* Using the client configured without encryption, find the document and observe + * that the field is not automatically decrypted. */ $document = $collection->findOne(); -print_r($document->encryptedField); -print_r($clientEncryption->decrypt($document->encryptedField)); +print_r($document); + +// Manually decrypt the field +printf("Decrypted: %s\n", $clientEncryption->decrypt($document->encryptedField)); diff --git a/docs/examples/csfle-explicit_encryption_automatic_decryption.php b/docs/examples/csfle-explicit_encryption_automatic_decryption.php index a1cbadf68..f53d67018 100644 --- a/docs/examples/csfle-explicit_encryption_automatic_decryption.php +++ b/docs/examples/csfle-explicit_encryption_automatic_decryption.php @@ -20,7 +20,7 @@ ], ]); -// Create a ClientEncryption object to manage data keys +// Create a ClientEncryption object to manage data encryption keys $clientEncryption = $client->createClientEncryption([ 'keyVaultNamespace' => 'encryption.__keyVault', 'kmsProviders' => [ @@ -28,12 +28,13 @@ ], ]); -/* Drop the key vault collection and create an encryption key. Alternatively, - * this key ID could be read from a configuration file. */ +/* Create a new key vault collection and data encryption key for this script. + * Alternatively, this key ID could be read from a configuration file. */ $client->selectCollection('encryption', '__keyVault')->drop(); +$client->selectCollection('encryption', '__keyVault')->createIndex(['keyAltNames' => 1], ['unique' => true]); $keyId = $clientEncryption->createDataKey('local'); -// Select and drop a collection to use for this example +// Create a new collection for this script $collection = $client->selectCollection('test', 'coll'); $collection->drop(); @@ -43,10 +44,10 @@ 'keyId' => $keyId, ]); -$collection->insertOne(['encryptedField' => $encryptedValue]); +$collection->insertOne(['_id' => 1, 'encryptedField' => $encryptedValue]); -/* Query for the document. The field will still be automatically decrypted - * because the client was configured with an autoEncryption driver option. */ +/* Using the client configured with encryption (but not automatic encryption), + * find the document and observe that the field is automatically decrypted. */ $document = $collection->findOne(); -print_r($document->encryptedField); +print_r($document); diff --git a/docs/examples/key_alt_name.php b/docs/examples/key_alt_name.php index b8dadc292..8071078bb 100644 --- a/docs/examples/key_alt_name.php +++ b/docs/examples/key_alt_name.php @@ -3,6 +3,7 @@ use MongoDB\BSON\Binary; use MongoDB\Client; use MongoDB\Driver\ClientEncryption; +use MongoDB\Driver\Exception\ServerException; require __DIR__ . '/../../vendor/autoload.php'; @@ -11,10 +12,10 @@ // Generate a secure local key to use for this script $localKey = new Binary(random_bytes(96)); -/* Create a client with no encryption options. Additionally, create a - * ClientEncryption object to manage data keys. */ +// Create a client with no encryption options $client = new Client($uri); +// Create a ClientEncryption object to manage data encryption keys $clientEncryption = $client->createClientEncryption([ 'keyVaultNamespace' => 'encryption.__keyVault', 'kmsProviders' => [ @@ -22,12 +23,22 @@ ], ]); -/* Drop the key vault collection and create an encryption key with an alternate - * name. This would typically be done during application deployment. To store - * the key ID for later use, you can use serialize() or var_export(). */ +/* Create a new key vault collection for this script. The application must also + * ensure that a unique index exists for keyAltNames. */ $client->selectCollection('encryption', '__keyVault')->drop(); +$client->selectCollection('encryption', '__keyVault')->createIndex(['keyAltNames' => 1], ['unique' => true]); + +// Create a data encryption key with an alternate name $clientEncryption->createDataKey('local', ['keyAltNames' => ['myDataKey']]); +/* Attempt to create a second key with the same name to demonstrate that the + * unique index is enforced. */ +try { + $clientEncryption->createDataKey('local', ['keyAltNames' => ['myDataKey']]); +} catch (ServerException $e) { + printf("Error creating key: %s\n", $e->getMessage()); +} + // Encrypt a value, using the "keyAltName" option instead of "keyId" $encryptedValue = $clientEncryption->encrypt('mySecret', [ 'algorithm' => ClientEncryption::AEAD_AES_256_CBC_HMAC_SHA_512_DETERMINISTIC, diff --git a/docs/examples/queryable_encryption-automatic.php b/docs/examples/queryable_encryption-automatic.php index 40dda5827..9fa530312 100644 --- a/docs/examples/queryable_encryption-automatic.php +++ b/docs/examples/queryable_encryption-automatic.php @@ -8,27 +8,27 @@ $uri = getenv('MONGODB_URI') ?: 'mongodb://127.0.0.1/'; -/* Create a local key for this script. In practice, this value would be read - * from a file, constant, or environment variable. Production apps should use - * a cloud provider instead of a local key. */ +// Generate a secure local key to use for this script $localKey = new Binary(random_bytes(96)); -/* Create a client with no encryption options. Additionally, create a - * ClientEncryption object to manage data keys. */ +// Create a client with no encryption options $client = new Client($uri); +// Create a ClientEncryption object to manage data encryption keys $clientEncryption = $client->createClientEncryption([ 'keyVaultNamespace' => 'encryption.__keyVault', 'kmsProviders' => ['local' => ['key' => $localKey]], ]); -// Drop the key vault collection and create two data keys (one for each encrypted field) +/* Create a new key vault collection and data encryption keys for this script. + * Alternatively, the key IDs could be read from a configuration file. */ $client->selectCollection('encryption', '__keyVault')->drop(); -$dataKeyId1 = $clientEncryption->createDataKey('local'); -$dataKeyId2 = $clientEncryption->createDataKey('local'); +$client->selectCollection('encryption', '__keyVault')->createIndex(['keyAltNames' => 1], ['unique' => true]); +$keyId1 = $clientEncryption->createDataKey('local'); +$keyId2 = $clientEncryption->createDataKey('local'); -/* Create a client with automatic encryption enabled. Define encryptedFields for - * the collection in encryptedFieldsMap. */ +/* Create another client with automatic encryption enabled. Configure the + * encrypted collection using the "encryptedFields" option. */ $encryptedClient = new Client($uri, [], [ 'autoEncryption' => [ 'keyVaultNamespace' => 'encryption.__keyVault', @@ -39,13 +39,13 @@ [ 'path' => 'encryptedIndexed', 'bsonType' => 'string', - 'keyId' => $dataKeyId1, + 'keyId' => $keyId1, 'queries' => ['queryType' => ClientEncryption::QUERY_TYPE_EQUALITY], ], [ 'path' => 'encryptedUnindexed', 'bsonType' => 'string', - 'keyId' => $dataKeyId2, + 'keyId' => $keyId2, ], ], ], @@ -53,24 +53,22 @@ ], ]); -/* Drop and create the collection. Each method will infer encryptedFields from - * the client and manage internal encryption collections automatically. */ +/* Create a new collection for this script. The drop and create helpers will + * infer encryptedFields from the client and manage internal encryption + * collections automatically. */ $encryptedClient->selectDatabase('test')->dropCollection('coll'); $encryptedClient->selectDatabase('test')->createCollection('coll'); $encryptedCollection = $encryptedClient->selectCollection('test', 'coll'); /* Using the encrypted client, insert a document and find it by querying on the * encrypted field. Fields will be automatically encrypted and decrypted. */ -$indexedValue = 'indexedValue'; -$unindexedValue = 'unindexedValue'; - $encryptedCollection->insertOne([ '_id' => 1, - 'encryptedIndexed' => $indexedValue, - 'encryptedUnindexed' => $unindexedValue, + 'encryptedIndexed' => 'indexedValue', + 'encryptedUnindexed' => 'unindexedValue', ]); -print_r($encryptedCollection->findOne(['encryptedIndexed' => $indexedValue])); +print_r($encryptedCollection->findOne(['encryptedIndexed' => 'indexedValue'])); /* Using the client configured without encryption, find the same document and * observe that fields are not automatically decrypted. */ diff --git a/docs/examples/queryable_encryption-explicit.php b/docs/examples/queryable_encryption-explicit.php index a09455ae7..aeac2876e 100644 --- a/docs/examples/queryable_encryption-explicit.php +++ b/docs/examples/queryable_encryption-explicit.php @@ -8,26 +8,26 @@ $uri = getenv('MONGODB_URI') ?: 'mongodb://127.0.0.1/'; -/* Create a local key for this script. In practice, this value would be read - * from a file, constant, or environment variable. Production apps should use - * a cloud provider instead of a local key. */ +// Generate a secure local key to use for this script $localKey = new Binary(random_bytes(96)); -/* Create a client with no encryption options. Additionally, create a - * ClientEncryption object to manage data keys. */ +// Create a client with no encryption options $client = new Client($uri); +// Create a ClientEncryption object to manage data encryption keys $clientEncryption = $client->createClientEncryption([ 'keyVaultNamespace' => 'encryption.__keyVault', 'kmsProviders' => ['local' => ['key' => $localKey]], ]); -// Drop the key vault collection and create two data keys (one for each encrypted field) +/* Create a new key vault collection and data encryption keys for this script. + * Alternatively, the key IDs could be read from a configuration file. */ $client->selectCollection('encryption', '__keyVault')->drop(); -$dataKeyId1 = $clientEncryption->createDataKey('local'); -$dataKeyId2 = $clientEncryption->createDataKey('local'); +$client->selectCollection('encryption', '__keyVault')->createIndex(['keyAltNames' => 1], ['unique' => true]); +$keyId1 = $clientEncryption->createDataKey('local'); +$keyId2 = $clientEncryption->createDataKey('local'); -// Create a client with automatic encryption disabled +// Create another client with automatic encryption disabled $encryptedClient = new Client($uri, [], [ 'autoEncryption' => [ 'keyVaultNamespace' => 'encryption.__keyVault', @@ -36,59 +36,56 @@ ], ]); -// Define encryptedFields for the collection +// Define encrypted fields for the collection $encryptedFields = [ 'fields' => [ [ 'path' => 'encryptedIndexed', 'bsonType' => 'string', - 'keyId' => $dataKeyId1, + 'keyId' => $keyId1, 'queries' => ['queryType' => ClientEncryption::QUERY_TYPE_EQUALITY], ], [ 'path' => 'encryptedUnindexed', 'bsonType' => 'string', - 'keyId' => $dataKeyId2, + 'keyId' => $keyId2, ], ], ]; -/* Drop and create the collection. Pass encryptedFields to each method to ensure - * that internal encryption collections are managed. */ +/* Create a new collection for this script. Pass encryptedFields to the drop and + * create helpers to ensure that internal encryption collections are managed. */ $encryptedClient->selectDatabase('test')->dropCollection('coll', ['encryptedFields' => $encryptedFields]); $encryptedClient->selectDatabase('test')->createCollection('coll', ['encryptedFields' => $encryptedFields]); -$collection = $encryptedClient->selectCollection('test', 'coll'); +$encryptedCollection = $encryptedClient->selectCollection('test', 'coll'); // Insert a document with manually encrypted fields -$indexedValue = 'indexedValue'; -$unindexedValue = 'unindexedValue'; - -$insertPayloadIndexed = $clientEncryption->encrypt($indexedValue, [ +$indexedInsertPayload = $clientEncryption->encrypt('indexedValue', [ 'algorithm' => ClientEncryption::ALGORITHM_INDEXED, 'contentionFactor' => 1, - 'keyId' => $dataKeyId1, + 'keyId' => $keyId1, ]); -$insertPayloadUnindexed = $clientEncryption->encrypt($unindexedValue, [ +$unindexedInsertPayload = $clientEncryption->encrypt('unindexedValue', [ 'algorithm' => ClientEncryption::ALGORITHM_UNINDEXED, - 'keyId' => $dataKeyId2, + 'keyId' => $keyId2, ]); -$collection->insertOne([ +$encryptedCollection->insertOne([ '_id' => 1, - 'encryptedIndexed' => $insertPayloadIndexed, - 'encryptedUnindexed' => $insertPayloadUnindexed, + 'encryptedIndexed' => $indexedInsertPayload, + 'encryptedUnindexed' => $unindexedInsertPayload, ]); /* Encrypt the payload for an "equality" query using the same key that was used - * to encrypt the insert payload. */ -$findPayload = $clientEncryption->encrypt($indexedValue, [ + * to encrypt the corresponding insert payload. */ +$indexedFindPayload = $clientEncryption->encrypt('indexedValue', [ 'algorithm' => ClientEncryption::ALGORITHM_INDEXED, 'queryType' => ClientEncryption::QUERY_TYPE_EQUALITY, 'contentionFactor' => 1, - 'keyId' => $dataKeyId1, + 'keyId' => $keyId1, ]); -/* Find the inserted document. Fields will still be automatically decrypted - * because the client was configured with an autoEncryption driver option. */ -print_r($collection->findOne(['encryptedIndexed' => $findPayload])); +/* Using the client configured with encryption (but not automatic encryption), + * find the document and observe that the fields are automatically decrypted. */ +print_r($encryptedCollection->findOne(['encryptedIndexed' => $findPayload])); diff --git a/docs/tutorial/encryption.txt b/docs/tutorial/encryption.txt index 3e7af07bd..b43e33331 100644 --- a/docs/tutorial/encryption.txt +++ b/docs/tutorial/encryption.txt @@ -92,24 +92,26 @@ Creating an Encryption Key To create an encryption key, create a :php:`MongoDB\\Driver\\ClientEncryption ` -instance with encryption options and create a new data key. The method will -return the key ID which can be used to reference the key later. You can also -pass multiple alternate names for this key and reference the key by these names -instead of the key ID. Creating a new data encryption key would typically be -done on initial deployment, but depending on your use case you may want to use -more than one encryption key or create them dynamically. +instance with encryption options and use the +:php:`createDataKey() ` +method. The method will return the key ID which can be used to reference the key +later. You can also pass multiple :ref:`alternate names ` for this key +and reference the key by these names instead of the key ID. + +Creating a new data encryption key would typically be done on initial +deployment, but depending on your use case you may want to use more than one +encryption key (e.g. user-specific encryption keys) or create them dynamically. .. literalinclude:: /examples/create_data_key.php :language: php +.. _alt_name: + Referencing Encryption Keys by an Alternative Name ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -While it is possible to create an encryption key every time data is encrypted, -this is not the recommended approach. Instead, you should create your encryption -keys depending on your use case, e.g. by creating a user-specific encryption -key. To reference keys in your software, you can use the ``keyAltName`` +To reference keys in your application, you can use the ``keyAltName`` attribute specified when creating the key. The following example creates an encryption key with an alternative name, which could be done when deploying the application. The script then encrypts data by referencing the key by its @@ -156,26 +158,7 @@ level encryption and use a object to create a new encryption key. -Providing Local Automatic Encryption Rules -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The following example uses the ``schemaMap`` auto encryption driver option to -define encrypted fields using a -:manual:`strict subset of the JSON schema syntax `. - -Specifying a ``schemaMap`` this way provides more security than relying on JSON -schemas obtained from the server. It protects against a malicious server -advertising a false JSON schema, which could trick the client into sending -unencrypted data that should be encrypted. - -JSON schemas specified in the ``schemaMap`` auto encryption driver option only -apply to configuring automatic client-side field level encryption. Other -validation rules in the JSON schema will not be enforced by the driver and will -result in an error. - -.. literalinclude:: /examples/csfle-automatic_encryption-local_schema.php - :language: php - +.. _server-side: Server-Side Field Level Encryption Enforcement ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -183,7 +166,7 @@ Server-Side Field Level Encryption Enforcement The MongoDB 4.2+ server supports using schema validation to enforce encryption of specific fields in a collection. This schema validation will prevent an application from inserting unencrypted values for any fields marked with the -:manual:`"encrypt" JSON schema keyword `. +:manual:`"encrypt" schema keyword `. The following example sets up a collection with automatic encryption using a ``$jsonSchema`` validator and @@ -195,6 +178,29 @@ decrypted when reading on the client side. :language: php +Providing Local Automatic Encryption Rules +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The following example uses the ``schemaMap`` auto encryption driver option to +define encrypted fields using a +:manual:`strict subset of the JSON schema syntax `. + +Using ``schemaMap`` in conjunction with a :ref:`server-side schema ` +provides more security than relying entirely on a schema obtained from the +server. It protects against a malicious server advertising a false schema, which +could trick the client into sending unencrypted data that should be encrypted. + +.. note:: + + Only :manual:`Encryption Schema syntax ` + can be used with the ``schemaMap`` option. Do not specify document validation + keywords in the automatic encryption rules. To define document validation + rules, configure :manual:`schema validation `. + +.. literalinclude:: /examples/csfle-automatic_encryption-local_schema.php + :language: php + + Explicit Encryption ~~~~~~~~~~~~~~~~~~~ @@ -258,7 +264,7 @@ Explicit encryption in Queryable Encryption is performed using the :php:`MongoDB\Driver\ClientEncryption::encrypt() ` and :php:`decrypt() ` methods. Although values must be explicitly encrypted (e.g. insertions, query criteria), automatic -decryption for queries is possible by configuring ``encryptedFields`` on the +*decryption* for queries is possible by configuring ``encryptedFields`` on the collection, as demonstrated in the following example: .. literalinclude:: /examples/queryable_encryption-explicit.php diff --git a/tests/ExamplesTest.php b/tests/ExamplesTest.php index 3c9002385..c1236f31d 100644 --- a/tests/ExamplesTest.php +++ b/tests/ExamplesTest.php @@ -210,7 +210,9 @@ public function testEncryptionExamples(string $file, string $expectedOutput): vo public static function provideEncryptionExamples(): Generator { - $expectedOutput = <<<'OUTPUT' + yield 'create_data_key' => [ + 'file' => __DIR__ . '/../docs/examples/create_data_key.php', + 'expectedOutput' => <<<'OUTPUT' MongoDB\BSON\Binary Object ( [data] => %a @@ -221,27 +223,24 @@ public static function provideEncryptionExamples(): Generator [data] => %a [type] => 6 ) -OUTPUT; - - yield 'create_data_key' => [ - 'file' => __DIR__ . '/../docs/examples/create_data_key.php', - 'expectedOutput' => $expectedOutput, +OUTPUT, ]; - $expectedOutput = <<<'OUTPUT' + yield 'key_alt_name' => [ + 'file' => __DIR__ . '/../docs/examples/key_alt_name.php', + 'expectedOutput' => <<<'OUTPUT' +Error creating key: E11000 duplicate key error %s: encryption.__keyVault%sdup key: { keyAltNames: "myDataKey" } MongoDB\BSON\Binary Object ( [data] => %a [type] => 6 ) -OUTPUT; - - yield 'key_alt_name' => [ - 'file' => __DIR__ . '/../docs/examples/key_alt_name.php', - 'expectedOutput' => $expectedOutput, +OUTPUT, ]; - $expectedOutput = <<<'OUTPUT' + yield 'csfle-automatic_encryption-local_schema' => [ + 'file' => __DIR__ . '/../docs/examples/csfle-automatic_encryption-local_schema.php', + 'expectedOutput' => <<<'OUTPUT' MongoDB\Model\BSONDocument Object ( [storage:ArrayObject:private] => Array @@ -265,14 +264,13 @@ public static function provideEncryptionExamples(): Generator ) ) -OUTPUT; - - yield 'csfle-automatic_encryption-local_schema' => [ - 'file' => __DIR__ . '/../docs/examples/csfle-automatic_encryption-local_schema.php', - 'expectedOutput' => $expectedOutput, +Error inserting document: Document failed validation +OUTPUT, ]; - $expectedOutput = <<<'OUTPUT' + yield 'csfle-automatic_encryption-server_side_schema' => [ + 'file' => __DIR__ . '/../docs/examples/csfle-automatic_encryption-server_side_schema.php', + 'expectedOutput' => <<<'OUTPUT' MongoDB\Model\BSONDocument Object ( [storage:ArrayObject:private] => Array @@ -296,34 +294,44 @@ public static function provideEncryptionExamples(): Generator ) ) -OUTPUT; - - yield 'csfle-automatic_encryption-server_side_schema' => [ - 'file' => __DIR__ . '/../docs/examples/csfle-automatic_encryption-server_side_schema.php', - 'expectedOutput' => $expectedOutput, +Error inserting document: Document failed validation +OUTPUT, ]; - $expectedOutput = <<<'OUTPUT' -MongoDB\BSON\Binary Object -( - [data] => %a - [type] => 6 -) -mySecret -OUTPUT; - yield 'csfle-explicit_encryption' => [ 'file' => __DIR__ . '/../docs/examples/csfle-explicit_encryption.php', - 'expectedOutput' => $expectedOutput, - ]; + 'expectedOutput' => <<<'OUTPUT' +MongoDB\Model\BSONDocument Object +( + [storage:ArrayObject:private] => Array + ( + [_id] => 1 + [encryptedField] => MongoDB\BSON\Binary Object + ( + [data] => %a + [type] => 6 + ) - $expectedOutput = <<<'OUTPUT' -mySecret -OUTPUT; + ) + +) +Decrypted: mySecret +OUTPUT, + ]; yield 'csfle-explicit_encryption_automatic_decryption' => [ 'file' => __DIR__ . '/../docs/examples/csfle-explicit_encryption_automatic_decryption.php', - 'expectedOutput' => $expectedOutput, + 'expectedOutput' => <<<'OUTPUT' +MongoDB\Model\BSONDocument Object +( + [storage:ArrayObject:private] => Array + ( + [_id] => 1 + [encryptedField] => mySecret + ) + +) +OUTPUT, ]; } @@ -343,7 +351,9 @@ public function testQueryableEncryptionExamples(string $file, string $expectedOu public static function provideQueryableEncryptionExamples(): Generator { - $expectedOutput = <<<'OUTPUT' + yield 'queryable_encryption-automatic' => [ + 'file' => __DIR__ . '/../docs/examples/queryable_encryption-automatic.php', + 'expectedOutput' => <<<'OUTPUT' MongoDB\Model\BSONDocument Object ( [storage:ArrayObject:private] => Array @@ -402,14 +412,12 @@ public static function provideQueryableEncryptionExamples(): Generator ) ) -OUTPUT; - - yield 'queryable_encryption-automatic' => [ - 'file' => __DIR__ . '/../docs/examples/queryable_encryption-automatic.php', - 'expectedOutput' => $expectedOutput, +OUTPUT, ]; - $expectedOutput = <<<'OUTPUT' + yield 'queryable_encryption-explicit' => [ + 'file' => __DIR__ . '/../docs/examples/queryable_encryption-explicit.php', + 'expectedOutput' => <<<'OUTPUT' MongoDB\Model\BSONDocument Object ( [storage:ArrayObject:private] => Array @@ -434,11 +442,7 @@ public static function provideQueryableEncryptionExamples(): Generator ) ) -OUTPUT; - - yield 'queryable_encryption-explicit' => [ - 'file' => __DIR__ . '/../docs/examples/queryable_encryption-explicit.php', - 'expectedOutput' => $expectedOutput, +OUTPUT, ]; } From e81560a33d33aaa04d4c89ae2673a73b40989cd0 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Mon, 10 Jul 2023 12:29:18 -0400 Subject: [PATCH 3/6] Create key vault index in key management scripts and remove setup code elsewhere This replaces the setup code with a top-of-script comment to avoid repetition. ExamplesTest now prepares the cleans up the necessary collections. Add a note about creating the partial, unique index on keyAltNames. --- docs/examples/create_data_key.php | 14 +++++--- ...sfle-automatic_encryption-local_schema.php | 11 ++++--- ...utomatic_encryption-server_side_schema.php | 11 ++++--- docs/examples/csfle-explicit_encryption.php | 15 ++++----- ...plicit_encryption_automatic_decryption.php | 15 ++++----- docs/examples/key_alt_name.php | 14 +++++--- .../queryable_encryption-automatic.php | 18 +++++----- .../queryable_encryption-explicit.php | 17 ++++++---- docs/tutorial/encryption.txt | 9 +++-- tests/ExamplesTest.php | 33 +++++++++++++++---- 10 files changed, 97 insertions(+), 60 deletions(-) diff --git a/docs/examples/create_data_key.php b/docs/examples/create_data_key.php index 4fdec0664..dbb89bf1f 100644 --- a/docs/examples/create_data_key.php +++ b/docs/examples/create_data_key.php @@ -14,6 +14,15 @@ // Create a client with no encryption options $client = new Client($uri); +/* Prepare the database for this script. Drop the key vault collection and + * ensure it has a unique index for keyAltNames. This would typically be done + * during application deployment. */ +$client->selectCollection('encryption', '__keyVault')->drop(); +$client->selectCollection('encryption', '__keyVault')->createIndex(['keyAltNames' => 1], [ + 'unique' => true, + 'partialFilterExpression' => ['keyAltNames' => ['$exists' => true]], +]); + // Create a ClientEncryption object to manage data encryption keys $clientEncryption = $client->createClientEncryption([ 'keyVaultNamespace' => 'encryption.__keyVault', @@ -22,11 +31,6 @@ ], ]); -/* Create a new key vault collection for this script. The application must also - * ensure that a unique index exists for keyAltNames. */ -$client->selectCollection('encryption', '__keyVault')->drop(); -$client->selectCollection('encryption', '__keyVault')->createIndex(['keyAltNames' => 1], ['unique' => true]); - /* Create a data encryption key. To store the key ID for later use, you can use * serialize(), var_export(), etc. */ $keyId = $clientEncryption->createDataKey('local'); diff --git a/docs/examples/csfle-automatic_encryption-local_schema.php b/docs/examples/csfle-automatic_encryption-local_schema.php index bfa8e0456..249d341b0 100644 --- a/docs/examples/csfle-automatic_encryption-local_schema.php +++ b/docs/examples/csfle-automatic_encryption-local_schema.php @@ -9,6 +9,10 @@ $uri = getenv('MONGODB_URI') ?: 'mongodb://127.0.0.1/'; +/* Note: this script assumes that the test database is empty and that the key + * vault collection exists and has a partial, unique index on keyAltNames (as + * demonstrated in the encryption key management scripts). */ + // Generate a secure local key to use for this script $localKey = new Binary(random_bytes(96)); @@ -23,10 +27,8 @@ ], ]); -/* Create a new key vault collection and data encryption key for this script. - * Alternatively, this key ID could be read from a configuration file. */ -$client->selectCollection('encryption', '__keyVault')->drop(); -$client->selectCollection('encryption', '__keyVault')->createIndex(['keyAltNames' => 1], ['unique' => true]); +/* Create a data encryption key. Alternatively, this key ID could be read from a + * configuration file. */ $keyId = $clientEncryption->createDataKey('local'); /* Define a JSON schema for the encrypted collection. Since this only utilizes @@ -61,7 +63,6 @@ * Note: without a server-side schema, another client could potentially insert * unencrypted data into the collection. Therefore, a local schema should always * be used in conjunction with a server-side schema. */ -$encryptedClient->selectDatabase('test')->dropCollection('coll'); $encryptedClient->selectDatabase('test')->createCollection('coll', ['validator' => ['$jsonSchema' => $schema]]); $encryptedCollection = $encryptedClient->selectCollection('test', 'coll'); diff --git a/docs/examples/csfle-automatic_encryption-server_side_schema.php b/docs/examples/csfle-automatic_encryption-server_side_schema.php index e47314a0e..a42ed6fb4 100644 --- a/docs/examples/csfle-automatic_encryption-server_side_schema.php +++ b/docs/examples/csfle-automatic_encryption-server_side_schema.php @@ -9,6 +9,10 @@ $uri = getenv('MONGODB_URI') ?: 'mongodb://127.0.0.1/'; +/* Note: this script assumes that the test database is empty and that the key + * vault collection exists and has a partial, unique index on keyAltNames (as + * demonstrated in the encryption key management scripts). */ + // Generate a secure local key to use for this script $localKey = new Binary(random_bytes(96)); @@ -23,10 +27,8 @@ ], ]); -/* Create a new key vault collection and data encryption key for this script. - * Alternatively, this key ID could be read from a configuration file. */ -$client->selectCollection('encryption', '__keyVault')->drop(); -$client->selectCollection('encryption', '__keyVault')->createIndex(['keyAltNames' => 1], ['unique' => true]); +/* Create a data encryption key. Alternatively, this key ID could be read from a + * configuration file. */ $keyId = $clientEncryption->createDataKey('local'); // Create another client with automatic encryption enabled @@ -53,7 +55,6 @@ /* Create a new collection for this script. Configure a server-side schema by * explicitly creating the collection with a "validator" option. */ -$encryptedClient->selectDatabase('test')->dropCollection('coll'); $encryptedClient->selectDatabase('test')->createCollection('coll', ['validator' => ['$jsonSchema' => $schema]]); $encryptedCollection = $encryptedClient->selectCollection('test', 'coll'); diff --git a/docs/examples/csfle-explicit_encryption.php b/docs/examples/csfle-explicit_encryption.php index ac2d0a59d..ce5ccf152 100644 --- a/docs/examples/csfle-explicit_encryption.php +++ b/docs/examples/csfle-explicit_encryption.php @@ -8,6 +8,10 @@ $uri = getenv('MONGODB_URI') ?: 'mongodb://127.0.0.1/'; +/* Note: this script assumes that the test database is empty and that the key + * vault collection exists and has a partial, unique index on keyAltNames (as + * demonstrated in the encryption key management scripts). */ + // Generate a secure local key to use for this script $localKey = new Binary(random_bytes(96)); @@ -22,22 +26,17 @@ ], ]); -/* Create a new key vault collection and data encryption key for this script. - * Alternatively, this key ID could be read from a configuration file. */ -$client->selectCollection('encryption', '__keyVault')->drop(); -$client->selectCollection('encryption', '__keyVault')->createIndex(['keyAltNames' => 1], ['unique' => true]); +/* Create a data encryption key. Alternatively, this key ID could be read from a + * configuration file. */ $keyId = $clientEncryption->createDataKey('local'); -// Create a new collection for this script -$collection = $client->selectCollection('test', 'coll'); -$collection->drop(); - // Insert a document with a manually encrypted field $encryptedValue = $clientEncryption->encrypt('mySecret', [ 'algorithm' => ClientEncryption::AEAD_AES_256_CBC_HMAC_SHA_512_DETERMINISTIC, 'keyId' => $keyId, ]); +$collection = $client->selectCollection('test', 'coll'); $collection->insertOne(['_id' => 1, 'encryptedField' => $encryptedValue]); /* Using the client configured without encryption, find the document and observe diff --git a/docs/examples/csfle-explicit_encryption_automatic_decryption.php b/docs/examples/csfle-explicit_encryption_automatic_decryption.php index f53d67018..4b98f95a7 100644 --- a/docs/examples/csfle-explicit_encryption_automatic_decryption.php +++ b/docs/examples/csfle-explicit_encryption_automatic_decryption.php @@ -8,6 +8,10 @@ $uri = getenv('MONGODB_URI') ?: 'mongodb://127.0.0.1/'; +/* Note: this script assumes that the test database is empty and that the key + * vault collection exists and has a partial, unique index on keyAltNames (as + * demonstrated in the encryption key management scripts). */ + // Generate a secure local key to use for this script $localKey = new Binary(random_bytes(96)); @@ -28,22 +32,17 @@ ], ]); -/* Create a new key vault collection and data encryption key for this script. - * Alternatively, this key ID could be read from a configuration file. */ -$client->selectCollection('encryption', '__keyVault')->drop(); -$client->selectCollection('encryption', '__keyVault')->createIndex(['keyAltNames' => 1], ['unique' => true]); +/* Create a data encryption key. Alternatively, this key ID could be read from a + * configuration file. */ $keyId = $clientEncryption->createDataKey('local'); -// Create a new collection for this script -$collection = $client->selectCollection('test', 'coll'); -$collection->drop(); - // Insert a document with a manually encrypted field $encryptedValue = $clientEncryption->encrypt('mySecret', [ 'algorithm' => ClientEncryption::AEAD_AES_256_CBC_HMAC_SHA_512_DETERMINISTIC, 'keyId' => $keyId, ]); +$collection = $client->selectCollection('test', 'coll'); $collection->insertOne(['_id' => 1, 'encryptedField' => $encryptedValue]); /* Using the client configured with encryption (but not automatic encryption), diff --git a/docs/examples/key_alt_name.php b/docs/examples/key_alt_name.php index 8071078bb..ea04cc209 100644 --- a/docs/examples/key_alt_name.php +++ b/docs/examples/key_alt_name.php @@ -15,6 +15,15 @@ // Create a client with no encryption options $client = new Client($uri); +/* Prepare the database for this script. Drop the key vault collection and + * ensure it has a unique index for keyAltNames. This would typically be done + * during application deployment. */ +$client->selectCollection('encryption', '__keyVault')->drop(); +$client->selectCollection('encryption', '__keyVault')->createIndex(['keyAltNames' => 1], [ + 'unique' => true, + 'partialFilterExpression' => ['keyAltNames' => ['$exists' => true]], +]); + // Create a ClientEncryption object to manage data encryption keys $clientEncryption = $client->createClientEncryption([ 'keyVaultNamespace' => 'encryption.__keyVault', @@ -23,11 +32,6 @@ ], ]); -/* Create a new key vault collection for this script. The application must also - * ensure that a unique index exists for keyAltNames. */ -$client->selectCollection('encryption', '__keyVault')->drop(); -$client->selectCollection('encryption', '__keyVault')->createIndex(['keyAltNames' => 1], ['unique' => true]); - // Create a data encryption key with an alternate name $clientEncryption->createDataKey('local', ['keyAltNames' => ['myDataKey']]); diff --git a/docs/examples/queryable_encryption-automatic.php b/docs/examples/queryable_encryption-automatic.php index 9fa530312..80bb770b8 100644 --- a/docs/examples/queryable_encryption-automatic.php +++ b/docs/examples/queryable_encryption-automatic.php @@ -8,6 +8,10 @@ $uri = getenv('MONGODB_URI') ?: 'mongodb://127.0.0.1/'; +/* Note: this script assumes that the test database is empty and that the key + * vault collection exists and has a partial, unique index on keyAltNames (as + * demonstrated in the encryption key management scripts). */ + // Generate a secure local key to use for this script $localKey = new Binary(random_bytes(96)); @@ -20,10 +24,8 @@ 'kmsProviders' => ['local' => ['key' => $localKey]], ]); -/* Create a new key vault collection and data encryption keys for this script. - * Alternatively, the key IDs could be read from a configuration file. */ -$client->selectCollection('encryption', '__keyVault')->drop(); -$client->selectCollection('encryption', '__keyVault')->createIndex(['keyAltNames' => 1], ['unique' => true]); +/* Create the data encryption keys for this script. Alternatively, the key IDs + * could be read from a configuration file. */ $keyId1 = $clientEncryption->createDataKey('local'); $keyId2 = $clientEncryption->createDataKey('local'); @@ -53,10 +55,10 @@ ], ]); -/* Create a new collection for this script. The drop and create helpers will - * infer encryptedFields from the client and manage internal encryption - * collections automatically. */ -$encryptedClient->selectDatabase('test')->dropCollection('coll'); +/* Create the data collection for this script. The create and drop helpers will + * infer encryptedFields from the client configuration and manage internal + * encryption collections automatically. Alternatively, the "encryptedFields" + * option can also be passed explicitly. */ $encryptedClient->selectDatabase('test')->createCollection('coll'); $encryptedCollection = $encryptedClient->selectCollection('test', 'coll'); diff --git a/docs/examples/queryable_encryption-explicit.php b/docs/examples/queryable_encryption-explicit.php index aeac2876e..ec29a72c2 100644 --- a/docs/examples/queryable_encryption-explicit.php +++ b/docs/examples/queryable_encryption-explicit.php @@ -8,6 +8,10 @@ $uri = getenv('MONGODB_URI') ?: 'mongodb://127.0.0.1/'; +/* Note: this script assumes that the test database is empty and that the key + * vault collection exists and has a partial, unique index on keyAltNames (as + * demonstrated in the encryption key management scripts). */ + // Generate a secure local key to use for this script $localKey = new Binary(random_bytes(96)); @@ -20,10 +24,8 @@ 'kmsProviders' => ['local' => ['key' => $localKey]], ]); -/* Create a new key vault collection and data encryption keys for this script. - * Alternatively, the key IDs could be read from a configuration file. */ -$client->selectCollection('encryption', '__keyVault')->drop(); -$client->selectCollection('encryption', '__keyVault')->createIndex(['keyAltNames' => 1], ['unique' => true]); +/* Create the data encryption keys. Alternatively, the key IDs could be read + * from a configuration file. */ $keyId1 = $clientEncryption->createDataKey('local'); $keyId2 = $clientEncryption->createDataKey('local'); @@ -53,9 +55,10 @@ ], ]; -/* Create a new collection for this script. Pass encryptedFields to the drop and - * create helpers to ensure that internal encryption collections are managed. */ -$encryptedClient->selectDatabase('test')->dropCollection('coll', ['encryptedFields' => $encryptedFields]); +/* Create the data collection for this script. Specify the "encryptedFields" + * option to ensure that internal encryption collections are also created. The + * "encryptedFields" option should also be specified when dropping the + * collection to ensure that internal encryption collections are dropped. */ $encryptedClient->selectDatabase('test')->createCollection('coll', ['encryptedFields' => $encryptedFields]); $encryptedCollection = $encryptedClient->selectCollection('test', 'coll'); diff --git a/docs/tutorial/encryption.txt b/docs/tutorial/encryption.txt index b43e33331..2773eab8c 100644 --- a/docs/tutorial/encryption.txt +++ b/docs/tutorial/encryption.txt @@ -115,10 +115,13 @@ To reference keys in your application, you can use the ``keyAltName`` attribute specified when creating the key. The following example creates an encryption key with an alternative name, which could be done when deploying the application. The script then encrypts data by referencing the key by its -alternative name. +alternative name using the ``keyAltName`` option instead of ``keyId``. -To use an alternate name when referencing an encryption key, use the -``keyAltName`` option instead of ``keyId``. +.. note:: + + Prior to adding a new key alternate name, you must create a partial, unique + index on the ``keyAltNames`` field. Client-Side Field Level Encryption + depends on server-enforced uniqueness of key alternate names. .. literalinclude:: /examples/key_alt_name.php :language: php diff --git a/tests/ExamplesTest.php b/tests/ExamplesTest.php index c1236f31d..3601f8c0a 100644 --- a/tests/ExamplesTest.php +++ b/tests/ExamplesTest.php @@ -201,11 +201,17 @@ public function testEncryptionExamples(string $file, string $expectedOutput): vo { $this->skipIfClientSideEncryptionIsNotSupported(); - $this->assertExampleOutput($file, $expectedOutput); - - // Clean up metadata and key vault collections + /* Ensure that the key vault, collection under test, and any metadata + * collections are cleaned up before and after the example is run. */ $this->dropCollection('test', 'coll', ['encryptedFields' => []]); $this->dropCollection('encryption', '__keyVault'); + + /* Ensure the key vault has a partial, unique index for keyAltNames. The + * key management examples already do this, so this is mainly for the + * benefit of other scripts. */ + $this->setUpKeyVaultIndex(); + + $this->assertExampleOutput($file, $expectedOutput); } public static function provideEncryptionExamples(): Generator @@ -342,11 +348,15 @@ public function testQueryableEncryptionExamples(string $file, string $expectedOu $this->skipIfServerVersion('<', '7.0.0', 'Queryable encryption tests require MongoDB 7.0 or later'); - $this->assertExampleOutput($file, $expectedOutput); - - // Clean up metadata and key vault collections + /* Ensure that the key vault, collection under test, and any metadata + * collections are cleaned up before and after the example is run. */ $this->dropCollection('test', 'coll', ['encryptedFields' => []]); $this->dropCollection('encryption', '__keyVault'); + + // Ensure the key vault has a partial, unique index for keyAltNames + $this->setUpKeyVaultIndex(); + + $this->assertExampleOutput($file, $expectedOutput); } public static function provideQueryableEncryptionExamples(): Generator @@ -452,4 +462,15 @@ private function assertExampleOutput(string $file, string $expectedOutput): void $this->assertStringMatchesFormat($expectedOutput, $this->getActualOutputForAssertion()); } + + private function setUpKeyVaultIndex(): void + { + self::createTestClient()->selectCollection('encryption', '__keyVault')->createIndex( + ['keyAltNames' => 1], + [ + 'unique' => true, + 'partialFilterExpression' => ['keyAltNames' => ['$exists' => true]], + ] + ); + } } From e05aa7d21b9e46fd7c6b1c32b182211656ee63fe Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Mon, 10 Jul 2023 12:32:08 -0400 Subject: [PATCH 4/6] Fix variable typo --- docs/examples/queryable_encryption-explicit.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/queryable_encryption-explicit.php b/docs/examples/queryable_encryption-explicit.php index ec29a72c2..678559644 100644 --- a/docs/examples/queryable_encryption-explicit.php +++ b/docs/examples/queryable_encryption-explicit.php @@ -91,4 +91,4 @@ /* Using the client configured with encryption (but not automatic encryption), * find the document and observe that the fields are automatically decrypted. */ -print_r($encryptedCollection->findOne(['encryptedIndexed' => $findPayload])); +print_r($encryptedCollection->findOne(['encryptedIndexed' => $indexedFindPayload])); From af018f125d7b27d5936189493fd059887169f43b Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Mon, 10 Jul 2023 14:21:10 -0400 Subject: [PATCH 5/6] Update baseline --- psalm-baseline.xml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index b761a397f..1e29faf7f 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -16,7 +16,8 @@ - + + $clientEncryption->decrypt($document->encryptedField) $document->encryptedField @@ -33,12 +34,6 @@ - - $document->encryptedField - - - $document->encryptedField - new Binary(random_bytes(96)) From 847023d53134895d03e2e40aefc62b3c2a458da7 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Mon, 10 Jul 2023 16:58:32 -0400 Subject: [PATCH 6/6] PHP 7.2 doesn't allow indenting nowdoc closing identifiers --- tests/ExamplesTest.php | 80 +++++++++++++++++++++++++----------------- 1 file changed, 48 insertions(+), 32 deletions(-) diff --git a/tests/ExamplesTest.php b/tests/ExamplesTest.php index 3601f8c0a..65c2741f1 100644 --- a/tests/ExamplesTest.php +++ b/tests/ExamplesTest.php @@ -216,9 +216,7 @@ public function testEncryptionExamples(string $file, string $expectedOutput): vo public static function provideEncryptionExamples(): Generator { - yield 'create_data_key' => [ - 'file' => __DIR__ . '/../docs/examples/create_data_key.php', - 'expectedOutput' => <<<'OUTPUT' + $expectedOutput = <<<'OUTPUT' MongoDB\BSON\Binary Object ( [data] => %a @@ -229,24 +227,28 @@ public static function provideEncryptionExamples(): Generator [data] => %a [type] => 6 ) -OUTPUT, +OUTPUT; + + yield 'create_data_key' => [ + 'file' => __DIR__ . '/../docs/examples/create_data_key.php', + 'expectedOutput' => $expectedOutput, ]; - yield 'key_alt_name' => [ - 'file' => __DIR__ . '/../docs/examples/key_alt_name.php', - 'expectedOutput' => <<<'OUTPUT' + $expectedOutput = <<<'OUTPUT' Error creating key: E11000 duplicate key error %s: encryption.__keyVault%sdup key: { keyAltNames: "myDataKey" } MongoDB\BSON\Binary Object ( [data] => %a [type] => 6 ) -OUTPUT, +OUTPUT; + + yield 'key_alt_name' => [ + 'file' => __DIR__ . '/../docs/examples/key_alt_name.php', + 'expectedOutput' => $expectedOutput, ]; - yield 'csfle-automatic_encryption-local_schema' => [ - 'file' => __DIR__ . '/../docs/examples/csfle-automatic_encryption-local_schema.php', - 'expectedOutput' => <<<'OUTPUT' + $expectedOutput = <<<'OUTPUT' MongoDB\Model\BSONDocument Object ( [storage:ArrayObject:private] => Array @@ -271,12 +273,14 @@ public static function provideEncryptionExamples(): Generator ) Error inserting document: Document failed validation -OUTPUT, +OUTPUT; + + yield 'csfle-automatic_encryption-local_schema' => [ + 'file' => __DIR__ . '/../docs/examples/csfle-automatic_encryption-local_schema.php', + 'expectedOutput' => $expectedOutput, ]; - yield 'csfle-automatic_encryption-server_side_schema' => [ - 'file' => __DIR__ . '/../docs/examples/csfle-automatic_encryption-server_side_schema.php', - 'expectedOutput' => <<<'OUTPUT' + $expectedOutput = <<<'OUTPUT' MongoDB\Model\BSONDocument Object ( [storage:ArrayObject:private] => Array @@ -301,12 +305,14 @@ public static function provideEncryptionExamples(): Generator ) Error inserting document: Document failed validation -OUTPUT, +OUTPUT; + + yield 'csfle-automatic_encryption-server_side_schema' => [ + 'file' => __DIR__ . '/../docs/examples/csfle-automatic_encryption-server_side_schema.php', + 'expectedOutput' => $expectedOutput, ]; - yield 'csfle-explicit_encryption' => [ - 'file' => __DIR__ . '/../docs/examples/csfle-explicit_encryption.php', - 'expectedOutput' => <<<'OUTPUT' + $expectedOutput = <<<'OUTPUT' MongoDB\Model\BSONDocument Object ( [storage:ArrayObject:private] => Array @@ -322,12 +328,14 @@ public static function provideEncryptionExamples(): Generator ) Decrypted: mySecret -OUTPUT, +OUTPUT; + + yield 'csfle-explicit_encryption' => [ + 'file' => __DIR__ . '/../docs/examples/csfle-explicit_encryption.php', + 'expectedOutput' => $expectedOutput, ]; - yield 'csfle-explicit_encryption_automatic_decryption' => [ - 'file' => __DIR__ . '/../docs/examples/csfle-explicit_encryption_automatic_decryption.php', - 'expectedOutput' => <<<'OUTPUT' + $expectedOutput = <<<'OUTPUT' MongoDB\Model\BSONDocument Object ( [storage:ArrayObject:private] => Array @@ -337,7 +345,11 @@ public static function provideEncryptionExamples(): Generator ) ) -OUTPUT, +OUTPUT; + + yield 'csfle-explicit_encryption_automatic_decryption' => [ + 'file' => __DIR__ . '/../docs/examples/csfle-explicit_encryption_automatic_decryption.php', + 'expectedOutput' => $expectedOutput, ]; } @@ -361,9 +373,7 @@ public function testQueryableEncryptionExamples(string $file, string $expectedOu public static function provideQueryableEncryptionExamples(): Generator { - yield 'queryable_encryption-automatic' => [ - 'file' => __DIR__ . '/../docs/examples/queryable_encryption-automatic.php', - 'expectedOutput' => <<<'OUTPUT' + $expectedOutput = <<<'OUTPUT' MongoDB\Model\BSONDocument Object ( [storage:ArrayObject:private] => Array @@ -422,12 +432,14 @@ public static function provideQueryableEncryptionExamples(): Generator ) ) -OUTPUT, +OUTPUT; + + yield 'queryable_encryption-automatic' => [ + 'file' => __DIR__ . '/../docs/examples/queryable_encryption-automatic.php', + 'expectedOutput' => $expectedOutput, ]; - yield 'queryable_encryption-explicit' => [ - 'file' => __DIR__ . '/../docs/examples/queryable_encryption-explicit.php', - 'expectedOutput' => <<<'OUTPUT' + $expectedOutput = <<<'OUTPUT' MongoDB\Model\BSONDocument Object ( [storage:ArrayObject:private] => Array @@ -452,7 +464,11 @@ public static function provideQueryableEncryptionExamples(): Generator ) ) -OUTPUT, +OUTPUT; + + yield 'queryable_encryption-explicit' => [ + 'file' => __DIR__ . '/../docs/examples/queryable_encryption-explicit.php', + 'expectedOutput' => $expectedOutput, ]; }