-
-
Notifications
You must be signed in to change notification settings - Fork 513
PHPORM-13 Feature Queryable Encryption #2779
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
GromNaN
wants to merge
35
commits into
2.12.x
Choose a base branch
from
feature/queryable-encryption
base: 2.12.x
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+2,090
−115
Open
Changes from all commits
Commits
Show all changes
35 commits
Select commit
Hold shift + click to select a range
3acd685
Add support for Field-Level Automatic and Queryable Encryption (#2759)
GromNaN 59ddb05
[Encryption] Set master key when creating an encrypted collection (#2…
GromNaN 8fc9d01
[Encryption] Refactor encrypted fields map generator (#2783)
alcaeus 4776dcf
Drop metadata collection when dropping encrypted collection (#2781)
GromNaN 9d7cdbc
Fix getDefaultKmsProvider method name (#2784)
GromNaN 5f42562
[Encryption] Fix BSON types and query option name (#2785)
GromNaN f852bd2
Remove encryptedFields option when dropping collection (#2788)
GromNaN 097c4d5
[Encryption] PHPORM-360 Document limitations of encryption with colle…
GromNaN bbb9134
[Encryption] Add cookbook for queryable encryption (#2793)
GromNaN 6c797fa
Fix typo in precision mapping option
alcaeus 20241f1
Fix typos
alcaeus 40c8b8e
Fix wrong namespace in documentation
alcaeus ae66c28
Remove duplicate field mapping in test document
alcaeus b72a0f6
Fix wrong embedded mapping in test document
alcaeus 33e150f
Fix wrong suggestion from Copilot
alcaeus 36ca89b
Support encrypted mappings of embedded documents in XML mappings (#2795)
alcaeus cf3c0b7
Cast KMS provider to object to support AWS empty config (#2801)
GromNaN 376fee4
Fix minimum PHP version with QE support
GromNaN 774834a
Add missing options in Configuration::setAutoEncryption
GromNaN 46fca5b
Remove trailing period from exception messages
GromNaN f3df7a8
Fix psalm type of field mapping
GromNaN c46fb02
Update test on dropping encrypted collection (#2803)
GromNaN 8f05ac1
Amend cf3c0b70 to cast only [] to an empty stdClass document
GromNaN f859c06
Remove maxOccurs=1
GromNaN 8299e1f
Update docs
GromNaN 44fb5e0
Add comment about EncryptConfig psalm type
GromNaN c183333
Rename loop variables
GromNaN 492c723
Add fields property to encryptedFieldsMap
GromNaN d9e9a75
Rename $path to $parentPath
GromNaN a0b8c69
Fix phpstan baseline
GromNaN dad3800
Update composer version constraint
GromNaN b7a7409
[Encryption] Add Int64 type (#2805)
GromNaN 5736515
Document default keyVaultNamespace
GromNaN ea87a8d
Rename getEncryptedFieldsForClass
GromNaN bd6834a
Use base64 local key in tests
GromNaN File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,270 @@ | ||
Queryable Encryption | ||
==================== | ||
|
||
This cookbook provides a tutorial on setting up and using Queryable Encryption | ||
(QE) with Doctrine MongoDB ODM to protect sensitive data in your documents. | ||
|
||
Introduction | ||
------------ | ||
|
||
In many applications, you need to store sensitive information like social | ||
security numbers, financial data, or personal details. MongoDB's Queryable | ||
Encryption allows you to encrypt this data on the client-side, store it as | ||
fully randomized encrypted data, and still run expressive queries on it. This | ||
ensures that sensitive data is never exposed in an unencrypted state on the | ||
server, in system logs, or in backups. | ||
|
||
This tutorial will guide you through the process of securing a document's | ||
fields using queryable encryption, from defining the document and configuring | ||
the connection to storing and querying the encrypted data. | ||
|
||
.. note:: | ||
|
||
Queryable Encryption is only available on MongoDB Enterprise 7.0+ or | ||
MongoDB Atlas. | ||
|
||
The Scenario | ||
------------ | ||
|
||
We will model a ``Patient`` document that has an embedded ``PatientRecord``. | ||
This record contains sensitive information: | ||
|
||
- A Social Security Number (``ssn``), which we need to query for exact | ||
matches. | ||
- A ``billingAmount``, which should support range queries. | ||
- A ``billing`` object, which should be encrypted but not directly queryable. | ||
|
||
Defining the Documents | ||
---------------------- | ||
|
||
First, let's define our ``Patient``, ``PatientRecord``, and ``Billing`` | ||
classes. We use the :ref:`#[Encrypt] <encrypt_attribute>` attribute to mark | ||
fields that require encryption. | ||
|
||
.. code-block:: php | ||
|
||
<?php | ||
|
||
namespace Documents; | ||
|
||
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; | ||
use Doctrine\ODM\MongoDB\Mapping\Annotations\Encrypt; | ||
use Doctrine\ODM\MongoDB\Mapping\Annotations\EncryptQuery; | ||
|
||
#[ODM\Document] | ||
class Patient | ||
{ | ||
#[ODM\Id] | ||
public string $id; | ||
|
||
#[ODM\EmbedOne(targetDocument: PatientRecord::class)] | ||
public PatientRecord $patientRecord; | ||
} | ||
|
||
#[ODM\EmbeddedDocument] | ||
class PatientRecord | ||
{ | ||
/** | ||
* Encrypted with equality queries. | ||
* This allows us to find a patient by their exact SSN. | ||
*/ | ||
#[ODM\Field(type: 'string')] | ||
#[Encrypt(queryType: EncryptQuery::Equality)] | ||
public string $ssn; | ||
|
||
/** | ||
* The entire embedded document is encrypted as an object. | ||
* By not specifying a queryType, we make it non-queryable. | ||
*/ | ||
#[ODM\EmbedOne(targetDocument: Billing::class)] | ||
#[Encrypt] | ||
public Billing $billing; | ||
|
||
/** | ||
* Encrypted with range queries. | ||
* This allows us to query for billing amounts within a certain range. | ||
*/ | ||
#[ODM\Field(type: 'int')] | ||
#[Encrypt(queryType: EncryptQuery::Range, min: 0, max: 5000, sparsity: 1)] | ||
public int $billingAmount; | ||
} | ||
|
||
#[ODM\EmbeddedDocument] | ||
class Billing | ||
{ | ||
#[ODM\Field(type: 'string')] | ||
public string $creditCardNumber; | ||
} | ||
|
||
Configuration and Usage | ||
----------------------- | ||
|
||
The following example demonstrates how to configure the ``DocumentManager`` for | ||
encryption and how to work with encrypted documents. | ||
|
||
Step 1: Configure the DocumentManager | ||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
First, we configure the ``DocumentManager`` with ``autoEncryption`` options. | ||
For more details on the available options, see the `MongoDB\Driver\Manager`_ | ||
documentation. We'll use the ``local`` KMS provider for simplicity. For this | ||
provider, you need a 96-byte master key. | ||
The following code will look for the key in a local file (``master-key.bin``) | ||
and generate it if it doesn't exist. In a production environment, you should | ||
use a non-local key management service (KMS). | ||
For each field marked with ``#[Encrypt]``, the MongoDB driver will generate | ||
a Data Encryption Key (DEK), encrypt it with the master key, and store it in | ||
the key vault collection. In Doctrine ODM, the key vault collection is set | ||
to ``<database>.datakeys`` by default, but you can change it using the | ||
``keyVaultNamespace`` option. | ||
|
||
.. code-block:: php | ||
|
||
<?php | ||
|
||
use Doctrine\ODM\MongoDB\Configuration; | ||
use Doctrine\ODM\MongoDB\Mapping\Driver\AttributeDriver; | ||
use MongoDB\BSON\Binary; | ||
|
||
// For the local KMS provider, we need a 96-byte master key. | ||
// We'll store it in a local file. If the file doesn't exist, we generate | ||
// one. In a production environment, ensure this key file is properly | ||
// secured. | ||
$keyFile = __DIR__ . '/master-key.bin'; | ||
if (!file_exists($keyFile)) { | ||
file_put_contents($keyFile, random_bytes(96)); | ||
} | ||
$masterKey = new Binary(file_get_contents($keyFile), Binary::TYPE_GENERIC); | ||
|
||
$config = new Configuration(); | ||
// Enable auto encryption and set the KMS provider. | ||
$config->setAutoEncryption([ | ||
'keyVaultNamespace' => 'encryption.datakeys' | ||
]); | ||
$config->setKmsProvider([ | ||
'type' => 'local', | ||
'key' => new Binary($masterKey), | ||
]); | ||
|
||
// Other configuration | ||
$config->setProxyDir(__DIR__ . '/Proxies'); | ||
$config->setProxyNamespace('Proxies'); | ||
$config->setHydratorDir(__DIR__ . '/Hydrators'); | ||
$config->setHydratorNamespace('Hydrators'); | ||
$config->setPersistentCollectionDir(__DIR__ . '/PersistentCollections'); | ||
$config->setPersistentCollectionNamespace('PersistentCollections'); | ||
$config->setDefaultDB('my_db'); | ||
$config->setMetadataDriverImpl(new AttributeDriver([__DIR__])); | ||
|
||
Step 2: Create the DocumentManager | ||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
The ``MongoDB\Client`` will be instantiated with the options from the | ||
configuration. | ||
|
||
.. code-block:: php | ||
|
||
<?php | ||
|
||
use Doctrine\ODM\MongoDB\DocumentManager; | ||
use MongoDB\Client; | ||
|
||
$client = new Client( | ||
uri: 'mongodb://localhost:27017/', | ||
uriOptions: [], | ||
driverOptions: $config->getDriverOptions(), | ||
); | ||
$documentManager = DocumentManager::create($client, $config); | ||
|
||
The ``driverOptions`` passed to the client contain the ``autoEncryption`` option | ||
that was configured in the previous step. | ||
|
||
Step 3: Create the Encrypted Collection | ||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
Next, we use the ``SchemaManager`` to create the collection with the necessary | ||
encryption metadata. To make the example re-runnable, we can drop the collection | ||
first. | ||
|
||
.. code-block:: php | ||
|
||
<?php | ||
|
||
$schemaManager = $documentManager->getSchemaManager(); | ||
$schemaManager->dropDocumentCollection(Patient::class); | ||
$schemaManager->createDocumentCollection(Patient::class); | ||
|
||
Step 4: Persist and Query Documents | ||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
Finally, we can persist and query documents as usual. The encryption and | ||
decryption will be handled automatically. | ||
|
||
.. code-block:: php | ||
|
||
<?php | ||
|
||
$patient = new Patient(); | ||
$patient->patientRecord = new PatientRecord(); | ||
$patient->patientRecord->ssn = '123-456-7890'; | ||
$patient->patientRecord->billingAmount = 1500; | ||
$patient->patientRecord->billing = new Billing(); | ||
$patient->patientRecord->billing->creditCardNumber = '9876-5432-1098-7654'; | ||
|
||
$documentManager->persist($patient); | ||
$documentManager->flush(); | ||
$documentManager->clear(); | ||
|
||
// Query the document using an encrypted field | ||
$foundPatient = $documentManager->getRepository(Patient::class)->findOneBy([ | ||
'patientRecord.ssn' => '123-456-7890', | ||
]); | ||
|
||
// The document is retrieved and its fields are automatically decrypted | ||
assert($foundPatient instanceof Patient); | ||
assert($foundPatient->patientRecord->billingAmount === 1500); | ||
|
||
What the Document Looks Like in the Database | ||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
When you inspect the document directly in the database (e.g., using ``mongosh`` | ||
or `MongoDB Compass`_), you will see that the fields marked with ``#[Encrypt]`` | ||
are stored as BSON binary data (subtype 6), not the original BSON type. The | ||
driver also adds a ``__safeContent__`` field to the document. For more details, | ||
see the `Queryable Encryption Fundamentals`_ in the MongoDB manual. | ||
|
||
.. code-block:: js | ||
|
||
{ | ||
"_id": ObjectId("..."), | ||
"patientRecord": { | ||
"ssn": Binary("...", 6), | ||
"billing": Binary("...", 6), | ||
"billingAmount": Binary("...", 6) | ||
}, | ||
"__safeContent__": [ | ||
Binary("...", 0) | ||
] | ||
} | ||
|
||
Limitations | ||
----------- | ||
|
||
- The ODM simplifies configuration by supporting a single KMS provider per | ||
``DocumentManager`` through ``Configuration::setKmsProvider()``. If you need | ||
to work with multiple KMS providers, you must manually configure the | ||
``kmsProviders`` array and pass it as a driver option, bypassing the ODM's | ||
helper method. | ||
- Automatic generation of the ``encryptedFieldsMap`` is not compatible with | ||
``SINGLE_COLLECTION`` inheritance. Because all classes in the hierarchy | ||
share a single collection, they must also share a single encryption schema. | ||
To use QE with inheritance, you must manually define the complete | ||
``encryptedFieldsMap`` for the entire hierarchy and provide it directly in | ||
the client options, bypassing the ODM's automatic generation. | ||
- For a complete list of hard limitations, please refer to the official | ||
`Queryable Encryption Limitations`_ documentation. | ||
|
||
.. _MongoDB\Driver\Manager: https://www.php.net/manual/en/mongodb-driver-manager.construct.php#mongodb-driver-manager.construct-autoencryption | ||
.. _MongoDB Compass: https://www.mongodb.com/products/compass | ||
.. _Queryable Encryption Fundamentals: https://www.mongodb.com/docs/manual/core/queryable-encryption/fundamentals/#behavior | ||
.. _Queryable Encryption Limitations: https://www.mongodb.com/docs/manual/core/queryable-encryption/reference/limitations/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it likely that users would run into conflicts attempting to do this? I suppose that depends on whether inheriting classes might have conflicting mappings (e.g. a field should be considered encrypted for one class but not another). If so, I wonder if it's worth warning users that this may be a bad idea.