diff --git a/docs/examples/create_data_key.php b/docs/examples/create_data_key.php new file mode 100644 index 000000000..dbb89bf1f --- /dev/null +++ b/docs/examples/create_data_key.php @@ -0,0 +1,46 @@ +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', + 'kmsProviders' => [ + 'local' => ['key' => $localKey], + ], +]); + +/* 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); + +// 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..249d341b0 --- /dev/null +++ b/docs/examples/csfle-automatic_encryption-local_schema.php @@ -0,0 +1,87 @@ +createClientEncryption([ + 'keyVaultNamespace' => 'encryption.__keyVault', + 'kmsProviders' => [ + 'local' => ['key' => $localKey], + ], +]); + +/* 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 + * 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' => $schema], + ], +]); + +/* 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')->createCollection('coll', ['validator' => ['$jsonSchema' => $schema]]); +$encryptedCollection = $encryptedClient->selectCollection('test', 'coll'); + +/* 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. */ +$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 new file mode 100644 index 000000000..a42ed6fb4 --- /dev/null +++ b/docs/examples/csfle-automatic_encryption-server_side_schema.php @@ -0,0 +1,79 @@ +createClientEncryption([ + 'keyVaultNamespace' => 'encryption.__keyVault', + 'kmsProviders' => [ + 'local' => ['key' => $localKey], + ], +]); + +/* 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 +$encryptedClient = new Client($uri, [], [ + 'autoEncryption' => [ + 'keyVaultNamespace' => 'encryption.__keyVault', + 'kmsProviders' => ['local' => ['key' => $localKey]], + ], +]); + +// 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')->createCollection('coll', ['validator' => ['$jsonSchema' => $schema]]); +$encryptedCollection = $encryptedClient->selectCollection('test', 'coll'); + +/* 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. */ +$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 new file mode 100644 index 000000000..ce5ccf152 --- /dev/null +++ b/docs/examples/csfle-explicit_encryption.php @@ -0,0 +1,49 @@ +createClientEncryption([ + 'keyVaultNamespace' => 'encryption.__keyVault', + 'kmsProviders' => [ + 'local' => ['key' => $localKey], + ], +]); + +/* Create a data encryption key. Alternatively, this key ID could be read from a + * configuration file. */ +$keyId = $clientEncryption->createDataKey('local'); + +// 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 + * that the field is not automatically decrypted. */ +$document = $collection->findOne(); + +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 new file mode 100644 index 000000000..4b98f95a7 --- /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 encryption keys +$clientEncryption = $client->createClientEncryption([ + 'keyVaultNamespace' => 'encryption.__keyVault', + 'kmsProviders' => [ + 'local' => ['key' => $localKey], + ], +]); + +/* Create a data encryption key. Alternatively, this key ID could be read from a + * configuration file. */ +$keyId = $clientEncryption->createDataKey('local'); + +// 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), + * find the document and observe that the field is automatically decrypted. */ +$document = $collection->findOne(); + +print_r($document); diff --git a/docs/examples/key_alt_name.php b/docs/examples/key_alt_name.php new file mode 100644 index 000000000..ea04cc209 --- /dev/null +++ b/docs/examples/key_alt_name.php @@ -0,0 +1,52 @@ +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', + 'kmsProviders' => [ + 'local' => ['key' => $localKey], + ], +]); + +// 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, + '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..80bb770b8 --- /dev/null +++ b/docs/examples/queryable_encryption-automatic.php @@ -0,0 +1,79 @@ +createClientEncryption([ + 'keyVaultNamespace' => 'encryption.__keyVault', + 'kmsProviders' => ['local' => ['key' => $localKey]], +]); + +/* 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'); + +/* Create another client with automatic encryption enabled. Configure the + * encrypted collection using the "encryptedFields" option. */ +$encryptedClient = new Client($uri, [], [ + 'autoEncryption' => [ + 'keyVaultNamespace' => 'encryption.__keyVault', + 'kmsProviders' => ['local' => ['key' => $localKey]], + 'encryptedFieldsMap' => [ + 'test.coll' => [ + 'fields' => [ + [ + 'path' => 'encryptedIndexed', + 'bsonType' => 'string', + 'keyId' => $keyId1, + 'queries' => ['queryType' => ClientEncryption::QUERY_TYPE_EQUALITY], + ], + [ + 'path' => 'encryptedUnindexed', + 'bsonType' => 'string', + 'keyId' => $keyId2, + ], + ], + ], + ], + ], +]); + +/* 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'); + +/* Using the encrypted client, insert a document and find it by querying on the + * encrypted field. Fields will be automatically encrypted and decrypted. */ +$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..678559644 --- /dev/null +++ b/docs/examples/queryable_encryption-explicit.php @@ -0,0 +1,94 @@ +createClientEncryption([ + 'keyVaultNamespace' => 'encryption.__keyVault', + 'kmsProviders' => ['local' => ['key' => $localKey]], +]); + +/* Create the data encryption keys. Alternatively, the key IDs could be read + * from a configuration file. */ +$keyId1 = $clientEncryption->createDataKey('local'); +$keyId2 = $clientEncryption->createDataKey('local'); + +// Create another client with automatic encryption disabled +$encryptedClient = new Client($uri, [], [ + 'autoEncryption' => [ + 'keyVaultNamespace' => 'encryption.__keyVault', + 'kmsProviders' => ['local' => ['key' => $localKey]], + 'bypassQueryAnalysis' => true, + ], +]); + +// Define encrypted fields for the collection +$encryptedFields = [ + 'fields' => [ + [ + 'path' => 'encryptedIndexed', + 'bsonType' => 'string', + 'keyId' => $keyId1, + 'queries' => ['queryType' => ClientEncryption::QUERY_TYPE_EQUALITY], + ], + [ + 'path' => 'encryptedUnindexed', + 'bsonType' => 'string', + 'keyId' => $keyId2, + ], + ], +]; + +/* 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'); + +// Insert a document with manually encrypted fields +$indexedInsertPayload = $clientEncryption->encrypt('indexedValue', [ + 'algorithm' => ClientEncryption::ALGORITHM_INDEXED, + 'contentionFactor' => 1, + 'keyId' => $keyId1, +]); + +$unindexedInsertPayload = $clientEncryption->encrypt('unindexedValue', [ + 'algorithm' => ClientEncryption::ALGORITHM_UNINDEXED, + 'keyId' => $keyId2, +]); + +$encryptedCollection->insertOne([ + '_id' => 1, + 'encryptedIndexed' => $indexedInsertPayload, + 'encryptedUnindexed' => $unindexedInsertPayload, +]); + +/* Encrypt the payload for an "equality" query using the same key that was used + * to encrypt the corresponding insert payload. */ +$indexedFindPayload = $clientEncryption->encrypt('indexedValue', [ + 'algorithm' => ClientEncryption::ALGORITHM_INDEXED, + 'queryType' => ClientEncryption::QUERY_TYPE_EQUALITY, + 'contentionFactor' => 1, + 'keyId' => $keyId1, +]); + +/* 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' => $indexedFindPayload])); 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..2773eab8c --- /dev/null +++ b/docs/tutorial/encryption.txt @@ -0,0 +1,274 @@ +================= +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 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 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +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 using 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 + + +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. + + +.. _server-side: + +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" 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 + + +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 +~~~~~~~~~~~~~~~~~~~ + +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..1e29faf7f 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,58 @@ + + + new Binary(random_bytes(96)) + + + + + new Binary(random_bytes(96)) + + + + + new Binary(random_bytes(96)) + + + + + $clientEncryption->decrypt($document->encryptedField) + $document->encryptedField + + + $document->encryptedField + + + $document->encryptedField + + + $document->encryptedField + + + new Binary(random_bytes(96)) + + + + + 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..65c2741f1 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,300 @@ 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(); + + /* 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 + { + $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' +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, + ]; + + $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 + ) + + ) + +) +Error inserting document: Document failed validation +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 + ) + + ) + +) +Error inserting document: Document failed validation +OUTPUT; + + yield 'csfle-automatic_encryption-server_side_schema' => [ + 'file' => __DIR__ . '/../docs/examples/csfle-automatic_encryption-server_side_schema.php', + 'expectedOutput' => $expectedOutput, + ]; + + $expectedOutput = <<<'OUTPUT' +MongoDB\Model\BSONDocument Object +( + [storage:ArrayObject:private] => Array + ( + [_id] => 1 + [encryptedField] => MongoDB\BSON\Binary Object + ( + [data] => %a + [type] => 6 + ) + + ) + +) +Decrypted: 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] => 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'); + + /* 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 + { + $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()); + } + + private function setUpKeyVaultIndex(): void + { + self::createTestClient()->selectCollection('encryption', '__keyVault')->createIndex( + ['keyAltNames' => 1], + [ + 'unique' => true, + 'partialFilterExpression' => ['keyAltNames' => ['$exists' => true]], + ] + ); } }