From 29db0f8d0482a925d990e0d35613cc44d65dfee6 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Fri, 30 Jun 2023 23:59:57 -0400 Subject: [PATCH] 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()); } }