PHP: полный мануал с примерами, функциями и ООП
Практический мануал PHP: от переменных и читаемого кода до безопасных форм, интеграций, базы данных и ООП на примере магазина. Учебный маршрут, поиск функций, подробные примеры, разбор ошибок и исходники для скачивания.

Дерево руководства
Глава 176 · После подключения к MongoDB; криптографические роли вводятся по шагам
MongoDB: выдать, найти и обслужить ключи для шифрования полей
Схема CMK → DEK → поле, выпуск ключа, точечный поиск, защита уникальности имён, переупаковка одного документа и холодная проверка удаления.
От кода получения к ключу с понятным именем
На складе вводят коды получения. Приложение должно находить свой ключ по имени pickup-code, даже когда обновляется один из его компонентов. Сначала создадим один ключ и прочитаем у него только имя. Затем проверим, что происходит с уже сохранённым талоном при изменении имён и обслуживании ключей.
- PHP создаёт 96 случайных байт для локального учебного CMK
- Драйвер создаёт DEK и защищает его этим CMK
- В vault сохраняется ключевой документ с UUID и меткой pickup-code
- PHP получает UUID, а по метке может найти тот же документ
Ключи образуют два уровня. DEK (data encryption key) шифрует само поле «код получения». CMK (customer master key) защищает этот DEK. Key vault — коллекция документов ключей, отдельная от коллекции заказов. Для начала представьте бирку и запертый футляр. Бирка помогает найти нужный ключ. Знание надписи на бирке не даёт возможности открыть футляр или прочитать поле.
$id = $ce->createDataKey('local', ['keyAltNames' => ['pickup-code']]);
echo $id->getType(); // 4До этого фрагмента создан $ce, объект ClientEncryption. Знак -> вызывает его метод. Строка local выбирает настроенный провайдер. В квадратных скобках передаётся список имён ключа. Метод вернул Binary в переменную $id; число 4 обозначает BSON-подтип UUID. Каждое выполнение создаёт новый ключ, поэтому при постоянном хранилище имя сначала планируют и обеспечивают его уникальность. Открыть полный пример первого ключа.
Подключить PHP к отдельной MongoDB · Зашифровать одно значение · Вернуть исходное значение.
Как устроены мастер-ключ, документ DEK и клиент
Хранилище называется в форме база.коллекция. В нём у DEK есть _id как 16-байтовый UUID, необязательный набор keyAltNames, данные о провайдере, даты создания и обновления, служебное состояние и обёрнутый keyMaterial. Для списка поддержки достаточно разрешённых названий и счётчиков. Для проверки UUID сравнивайте значения Binary в памяти.
$master = new MongoDB\BSON\Binary(random_bytes(96), MongoDB\BSON\Binary::TYPE_GENERIC);
$options = [
'keyVaultNamespace' => $labDatabase.'.'.$ownedVaultName,
'kmsProviders' => ['local' => ['key' => $master]],
];
$ce = $manager->createClientEncryption($options);Этот фрагмент запускает лабораторную схему с одним новым CMK. Учебные программы не берут ключ из пользовательского ввода и не публикуют содержимое документов. В постоянном приложении нужен проект управления ключами, доступом и резервными копиями. Генерирование нового CMK при каждом старте сделало бы старые постоянные данные недоступными для свежего клиента.
| Параметр | Что передают и где используется |
|---|---|
| keyVaultNamespace и keyVaultClient | Имя коллекции ключей и Manager, через который её читают. У прямого конструктора Manager обязателен; у фабрики он может быть текущим. |
| kmsProviders.local.key | CMK как 96 байт BSON Binary или их Base64. Здесь значение создаётся случайно в памяти одного процесса. |
| kmsProviders.aws / azure / gcp / kmip | Параметры подключения и аутентификации выбранного KMS. Приложение передаёт только конфигурацию своего провайдера. У AWS возможны временные credentials с sessionToken. Некоторые облачные провайдеры поддерживают автоматическую конфигурацию учётных данных. |
| masterKey при выдаче DEK | Для AWS указываются region и ARN key; для Azure — keyVaultEndpoint и keyName, при необходимости keyVersion; для GCP — projectId, location, keyRing, keyName и при необходимости версия/endpoint. Для KMIP — настройки идентификатора и endpoint, опциональный delegated. Для local документ не требуется. |
| tlsOptions | Опции доверенных CA и клиентского сертификата по имени внешнего провайдера. Держите пути и учётные данные в конфигурации среды. Точные параметры смотрите у конструктора. |
| keyAltNames и keyMaterial при создании DEK | Имена — опциональный список строк. Параметр 96-байтового материала DEK доступен c 1.15. В примерах драйвер генерирует материал самостоятельно. |
В классическом CSFLE константы AEAD_AES_256_CBC_HMAC_SHA_512_DETERMINISTIC и AEAD_AES_256_CBC_HMAC_SHA_512_RANDOM задают соответственно детерминированное и рандомизированное шифрование. Для другой технологии, Queryable Encryption, определены ALGORITHM_INDEXED, ALGORITHM_UNINDEXED и QUERY_TYPE_EQUALITY с 1.14, а также ALGORITHM_RANGE и QUERY_TYPE_RANGE с 1.20. Названия RANGE_PREVIEW были экспериментальными с 1.16 и удалены в 2.0. Запросы QE требуют соответствующей коллекции и настроек клиента. Официальное описание encryptExpression для диапазонов.
Почему имя ключа должно быть уникальным
- Для vault создать индекс keyAltNames: 1
- Включить только документы с этим полем
- MongoDB допускает имя packing у одного DEK
- Второму ключу остаётся его собственное имя billing
$index = [
'name' => 'unique_alt_names', 'key' => ['keyAltNames' => 1],
'unique' => true, 'partialFilterExpression' => ['keyAltNames' => ['$exists' => true]],
];Схема индекса передаётся в createIndexes учебного хранилища до выдачи DEK. Уникальный индекс на массиве имён не позволяет одной строке принадлежать двум разным DEK. Частичный фильтр пропускает документы без keyAltNames. Такой индекс создаётся в каждой изолированной коллекции примеров. Посмотреть конфликт имён и состояние обеих записей.
Как получить таблицу из работающего стенда
Для реального запуска требуется версия расширения с поддержкой libmongocrypt, отдельный MongoDB Community standalone на loopback и вымышленные данные. Проверяемый набор: PHP CLI 8.5.11 x64, ext-mongodb 2.5.5, MongoDB 8.0.34. Для явного CSFLE с локальным провайдером не требуется внешний сервис KMS, компонент автоматического анализа команд или облачный кластер.
php --ri mongodb
$env:PHPMAN_MONGODB_URI='mongodb://127.0.0.1:27075/?directConnection=true&retryWrites=false'
$env:PHPMAN_MONGODB_DATABASE='php_manual_lab'
php example.phpРаспакуйте файлы в отдельную папку и запустите example.php. Откройте result.html; тот же набор строк есть в result.json. Один запуск создаёт одну новую vault-коллекцию; для переупаковки и удаления добавляется одна бизнес-коллекция с учебным талоном. В finally удаляются только свои коллекции. Для постоянного приложения процедуры очистки, восстановления и ротации разрабатываются отдельно.
Выдать ключ для поля «код получения» и получить его идентификатор
Пункт выдачи хочет шифровать код получения. Первое практическое действие — один раз создать ключ данных и дать ему читаемое имя. После этого приложение сможет ссылаться на ключ по UUID или по имени pickup-code.
- Создать пустое хранилище и индекс имён
- Драйвер создаёт DEK и упаковывает его с помощью локального CMK
- MongoDB сохраняет документ ключа
- PHP получает UUID подтипа 4 и по нему читает имена
Короткий вызов
$id = $ce->createDataKey('local', ['keyAltNames' => ['pickup-code']]);
echo $id->getType(); // 4, BSON UUIDРезультат запуска
| Что проверили | Результат |
|---|---|
| Ключей до выдачи | 0 |
| Подтип BSON идентификатора | 4 |
| Имя для приложения | pickup-code |
| Провайдер | local |
Почему получился такой результат
CMK — главный ключ для защиты ключей данных. DEK — ключ, которым будет зашифровано конкретное значение. UUID обозначает документ DEK, его допустимо передавать в getKey. Поле keyMaterial внутри документа содержит обёрнутый материал; на страницу выводятся только метка и название провайдера.
Границы применения
Создание DEK в каждом веб-запросе привело бы к множеству ненужных ключей. У постоянного сервиса выпуск ключа и обработку заказа разделяют. Учебный CMK здесь намеренно живёт только в одном процессе.
Полная самостоятельная программа и архив
<?php
declare(strict_types=1);
use MongoDB\BSON\Binary;
use MongoDB\Driver\{ClientEncryption, Command, Manager, ReadPreference, Server};
use MongoDB\Driver\Exception\EncryptionException;
require __DIR__.'/kv132-helper.php';
// Keep driver messages, arguments and key documents outside public output.
set_exception_handler(static function(Throwable $error): void {
fwrite(STDERR, 'Vault example failed: '.$error::class.PHP_EOL);
exit(1);
});
if (!in_array(strtolower((string)ini_get('mongodb.debug')), ['', '0', 'off', 'no', 'false'], true)) {
throw new RuntimeException('Disable MongoDB debug tracing for this lab');
}
$settings = kv132Settings();
$uri = $settings['uri'];
$db = $settings['database'];
$manager = new Manager($uri, ['serverSelectionTimeoutMS'=>1500, 'connectTimeoutMS'=>500, 'socketTimeoutMS'=>3000]);
$server = $manager->selectServer(new ReadPreference(ReadPreference::PRIMARY));
if ($server->getType() !== Server::TYPE_STANDALONE) {
throw new RuntimeException('Use a dedicated standalone server');
}
$runtime = ['php'=>PHP_VERSION,'extension'=>phpversion('mongodb'),
'server'=>kv132Command($manager,$db,['buildInfo'=>1])[0]['version'],
'execution'=>'database','observed_host'=>$server->getHost(),'observed_port'=>$server->getPort()];
$owned = [];
try {
$vault = kv132Create($manager, $db, $owned);
$vaultName = $owned[0];
kv132Command($manager,$db,['createIndexes'=>$vaultName, 'indexes'=>[[
'name'=>'unique_alt_names', 'key'=>['keyAltNames'=>1], 'unique'=>true,
'partialFilterExpression'=>['keyAltNames'=>['$exists'=>true]],
]]],true);
$master = new Binary(random_bytes(96), Binary::TYPE_GENERIC);
$provider = ['local'=>['key'=>$master]];
$ce = $manager->createClientEncryption(['keyVaultNamespace'=>$vault, 'kmsProviders'=>$provider]);
$empty = iterator_count($ce->getKeys());
$id = $ce->createDataKey('local', ['keyAltNames'=>['pickup-code']]);
$document = $ce->getKey($id);
$names = kv132AliasList($document);
$providerName = $document->masterKey->provider;
$checks = ['empty_at_start'=>$empty === 0, 'uuid'=>$id->getType() === Binary::TYPE_UUID,
'sixteen_bytes'=>strlen($id->getData()) === 16, 'id_matches'=>$document->_id == $id,
'label'=>$names === ['pickup-code'], 'provider'=>$providerName === 'local',
'wrapped_material'=>$document->keyMaterial instanceof Binary];
$rows = [['Ключей до выдачи',$empty], ['Подтип BSON идентификатора',$id->getType()],
['Имя для приложения',$names[0]], ['Провайдер',$providerName]];
} finally {
foreach (array_reverse($owned) as $collection) {
kv132Command($manager,$db,['drop'=>$collection],true);
}
}
$removed = true;
foreach ($owned as $collection) {
if (kv132Exists($manager,$db,$collection)) {$removed = false;}
}
$checks['owned_collections_removed'] = $removed && count($owned) === 1;
kv132Report('mongodb132-vault-first-key', ['Что проверили', 'Результат'], $rows, $checks, $runtime);
echo 'mongodb132-vault-first-key: OK', PHP_EOL;
Ожидаемый результат
mongodb132-vault-first-key: OK
Работа с собственным временным хранилищем и локальным учебным CMK. На страницу попадают только разрешённые поля и результаты сравнений.
Открыть фактическую таблицу · Данные результата в JSON
Упражнение. Создайте второй учебный DEK с именем returns-note и подсчитайте документы vault. Для обоих будет тип UUID 4, но сами идентификаторы различаются.
Разработчик отправлял имя метода key vault как команду серверу. В этой главе поиск выполняется методом клиента расширения. Finding a DEK by alternate name на Stack Overflow.
Найти один ключ из двух клиентов с одинаковой конфигурацией
Два компонента PHP собираются работать с возвратами заказов. Один создаёт объект через фабрику Manager, другой получает Manager явно. Проверим, с каким хранилищем они работают.
- Фабрика использует текущий Manager
- В прямой конструктор передаём keyVaultClient
- Оба клиента читают указанную vault-коллекцию
- Сравниваем UUID при поиске по ID и имени
Короткий вызов
$factory = $manager->createClientEncryption($options);
$direct = new MongoDB\Driver\ClientEncryption($options + ['keyVaultClient' => $manager]);Результат запуска
| Что проверили | Результат |
|---|---|
| Созданы разные объекты PHP | true |
| Обе ссылки нашли один UUID | true |
| Ключ returns найден | true |
| Неизвестное имя найдено | false |
Почему получился такой результат
В $options заданы keyVaultNamespace и kmsProviders. Фабрика может выбрать свой Manager. Прямому конструктору нужен явно указанный keyVaultClient. Настройки local для обоих построены из одного временного мастер-ключа. Ответ по отсутствующему имени — null.
Границы применения
Сам lookup возвращает метаданные ключа, если есть доступ к коллекции. Он не доказывает, что текущий CMK сможет расшифровать данные. Для этого в примере про переупаковку есть свежий клиент и реальная проверка decrypt.
Полная самостоятельная программа и архив
<?php
declare(strict_types=1);
use MongoDB\BSON\Binary;
use MongoDB\Driver\{ClientEncryption, Command, Manager, ReadPreference, Server};
use MongoDB\Driver\Exception\EncryptionException;
require __DIR__.'/kv132-helper.php';
// Keep driver messages, arguments and key documents outside public output.
set_exception_handler(static function(Throwable $error): void {
fwrite(STDERR, 'Vault example failed: '.$error::class.PHP_EOL);
exit(1);
});
if (!in_array(strtolower((string)ini_get('mongodb.debug')), ['', '0', 'off', 'no', 'false'], true)) {
throw new RuntimeException('Disable MongoDB debug tracing for this lab');
}
$settings = kv132Settings();
$uri = $settings['uri'];
$db = $settings['database'];
$manager = new Manager($uri, ['serverSelectionTimeoutMS'=>1500, 'connectTimeoutMS'=>500, 'socketTimeoutMS'=>3000]);
$server = $manager->selectServer(new ReadPreference(ReadPreference::PRIMARY));
if ($server->getType() !== Server::TYPE_STANDALONE) {
throw new RuntimeException('Use a dedicated standalone server');
}
$runtime = ['php'=>PHP_VERSION,'extension'=>phpversion('mongodb'),
'server'=>kv132Command($manager,$db,['buildInfo'=>1])[0]['version'],
'execution'=>'database','observed_host'=>$server->getHost(),'observed_port'=>$server->getPort()];
$owned = [];
try {
$vault = kv132Create($manager, $db, $owned);
$vaultName = $owned[0];
kv132Command($manager,$db,['createIndexes'=>$vaultName, 'indexes'=>[[
'name'=>'unique_alt_names', 'key'=>['keyAltNames'=>1], 'unique'=>true,
'partialFilterExpression'=>['keyAltNames'=>['$exists'=>true]],
]]],true);
$master = new Binary(random_bytes(96), Binary::TYPE_GENERIC);
$provider = ['local'=>['key'=>$master]];
$ce = $manager->createClientEncryption(['keyVaultNamespace'=>$vault, 'kmsProviders'=>$provider]);
$id = $ce->createDataKey('local', ['keyAltNames'=>['returns']]);
$direct = new ClientEncryption(['keyVaultClient'=>$manager,'keyVaultNamespace'=>$vault,'kmsProviders'=>$provider]);
$fromFactory = $ce->getKey($id);
$fromConstructor = $direct->getKeyByAltName('returns');
$absent = $direct->getKeyByAltName('unknown-role');
$checks = ['factory_instance'=>$ce instanceof ClientEncryption, 'constructor_instance'=>$direct instanceof ClientEncryption,
'separate_php_instances'=>$ce !== $direct, 'factory_found'=>$fromFactory->_id == $id,
'same_record'=>$fromConstructor->_id == $id, 'known_alias'=>kv132AliasList($fromConstructor) === ['returns'], 'absent'=>$absent === null];
$rows = [['Созданы разные объекты PHP',$ce !== $direct],['Обе ссылки нашли один UUID',$fromFactory->_id == $fromConstructor->_id],
['Ключ returns найден',$fromConstructor !== null],['Неизвестное имя найдено',$absent !== null]];
} finally {
foreach (array_reverse($owned) as $collection) {
kv132Command($manager,$db,['drop'=>$collection],true);
}
}
$removed = true;
foreach ($owned as $collection) {
if (kv132Exists($manager,$db,$collection)) {$removed = false;}
}
$checks['owned_collections_removed'] = $removed && count($owned) === 1;
kv132Report('mongodb132-vault-client-options', ['Что проверили', 'Результат'], $rows, $checks, $runtime);
echo 'mongodb132-vault-client-options: OK', PHP_EOL;
Ожидаемый результат
mongodb132-vault-client-options: OK
Работа с собственным временным хранилищем и локальным учебным CMK. На страницу попадают только разрешённые поля и результаты сравнений.
Открыть фактическую таблицу · Данные результата в JSON
Упражнение. Передайте второму клиенту другую собственную пустую vault-коллекцию и попытайтесь найти returns. Убедитесь, что namespace входит в конфигурацию приложения.
Разработчик спрашивает, в какой системе открывается DEK и кто имеет доступ к мастер-ключу. Разбор отделяет local KMS от хранения документов ключей в коллекции. Field-level encryption and an external vault на Stack Overflow.
Составить список ролей, не выгружая содержимое ключей
Поддержка хочет знать, есть ли ключи для выдачи и для возвратов. Выведем алфавитный список учебных ролей и отдельно число ключей, которых никто не назвал.
- Создать три DEK, один без имени
- Получить Cursor хранилища
- Для каждой строки собрать только имена
- Посчитать безымянные документы и отсортировать роли
Короткий вызов
foreach ($ce->getKeys() as $document) {
foreach ($document->keyAltNames ?? [] as $name) {
// Включить в отчёт только разрешённое имя.
}
}Результат запуска
| Что проверили | Результат |
|---|---|
| Всего ключей | 3 |
| Без имени | 1 |
| Имена в отчёте | pickup, returns |
Почему получился такой результат
getKeys возвращает курсор по всем документам ключей. Во время обхода счётчик растёт на один документ. Сортируются лишь два отобранных демонстрационных имени. Безымянный ключ можно найти по UUID.
Границы применения
Для большого хранилища полный проход обходится пропорционально числу документов и читает служебные поля. Если нужен известный uuid или псевдоним, используйте точечный getKey или getKeyByAltName.
Полная самостоятельная программа и архив
<?php
declare(strict_types=1);
use MongoDB\BSON\Binary;
use MongoDB\Driver\{ClientEncryption, Command, Manager, ReadPreference, Server};
use MongoDB\Driver\Exception\EncryptionException;
require __DIR__.'/kv132-helper.php';
// Keep driver messages, arguments and key documents outside public output.
set_exception_handler(static function(Throwable $error): void {
fwrite(STDERR, 'Vault example failed: '.$error::class.PHP_EOL);
exit(1);
});
if (!in_array(strtolower((string)ini_get('mongodb.debug')), ['', '0', 'off', 'no', 'false'], true)) {
throw new RuntimeException('Disable MongoDB debug tracing for this lab');
}
$settings = kv132Settings();
$uri = $settings['uri'];
$db = $settings['database'];
$manager = new Manager($uri, ['serverSelectionTimeoutMS'=>1500, 'connectTimeoutMS'=>500, 'socketTimeoutMS'=>3000]);
$server = $manager->selectServer(new ReadPreference(ReadPreference::PRIMARY));
if ($server->getType() !== Server::TYPE_STANDALONE) {
throw new RuntimeException('Use a dedicated standalone server');
}
$runtime = ['php'=>PHP_VERSION,'extension'=>phpversion('mongodb'),
'server'=>kv132Command($manager,$db,['buildInfo'=>1])[0]['version'],
'execution'=>'database','observed_host'=>$server->getHost(),'observed_port'=>$server->getPort()];
$owned = [];
try {
$vault = kv132Create($manager, $db, $owned);
$vaultName = $owned[0];
kv132Command($manager,$db,['createIndexes'=>$vaultName, 'indexes'=>[[
'name'=>'unique_alt_names', 'key'=>['keyAltNames'=>1], 'unique'=>true,
'partialFilterExpression'=>['keyAltNames'=>['$exists'=>true]],
]]],true);
$master = new Binary(random_bytes(96), Binary::TYPE_GENERIC);
$provider = ['local'=>['key'=>$master]];
$ce = $manager->createClientEncryption(['keyVaultNamespace'=>$vault, 'kmsProviders'=>$provider]);
$ce->createDataKey('local', ['keyAltNames'=>['returns']]);
$ce->createDataKey('local', ['keyAltNames'=>['pickup']]);
$unlabelled = $ce->createDataKey('local');
$cursor = $ce->getKeys();
$roles = [];
$total = 0;
$withoutName = 0;
foreach ($cursor as $document) {
$total++;
$names = kv132AliasList($document);
if (!$names) {$withoutName++;}
foreach ($names as $label) {$roles[] = $label;}
}
sort($roles, SORT_STRING);
$found = $ce->getKey($unlabelled);
$checks = ['real_cursor'=>$cursor instanceof MongoDB\Driver\Cursor,'total'=>$total === 3,
'one_unlabelled'=>$withoutName === 1,'visible_role_names'=>$roles === ['pickup','returns'],
'uuid_lookup'=>$found->_id == $unlabelled,'no_name_in_record'=>kv132AliasList($found) === []];
$rows = [['Всего ключей',$total],['Без имени',$withoutName],['Имена в отчёте',implode(', ', $roles)]];
} finally {
foreach (array_reverse($owned) as $collection) {
kv132Command($manager,$db,['drop'=>$collection],true);
}
}
$removed = true;
foreach ($owned as $collection) {
if (kv132Exists($manager,$db,$collection)) {$removed = false;}
}
$checks['owned_collections_removed'] = $removed && count($owned) === 1;
kv132Report('mongodb132-vault-inventory', ['Что проверили', 'Результат'], $rows, $checks, $runtime);
echo 'mongodb132-vault-inventory: OK', PHP_EOL;
Ожидаемый результат
mongodb132-vault-inventory: OK
Работа с собственным временным хранилищем и локальным учебным CMK. На страницу попадают только разрешённые поля и результаты сравнений.
Открыть фактическую таблицу · Данные результата в JSON
Упражнение. Добавьте второе имя к одному из существующих ключей. Проверьте, что число увиденных имён увеличится, а число документов останется равным трём.
Переименовать ключ с переходным периодом для старого клиента
Сервис доставки постепенно переводит компоненты со старого имени ключа на новое. Пока обновлены только некоторые компоненты, оба имени должны вести к одному DEK.
- Создать dispatch-v1
- Добавить dispatch-v2 и проверить оба имени
- После перехода снять старое имя
- Сверить UUID и оставшийся псевдоним
Короткий вызов
$previous = $ce->addKeyAltName($id, 'dispatch-v2');
$current = $ce->getKey($id);Результат запуска
| Что проверили | Результат |
|---|---|
| До добавления | dispatch-v1 |
| В переходный период | dispatch-v1, dispatch-v2 |
| После перехода | dispatch-v2 |
| UUID сохранился | true |
Почему получился такой результат
addKeyAltName и removeKeyAltName возвращают версию документа, которая существовала до изменения. Новый снимок получается отдельным getKey. Строки таблицы показывают последовательную миграцию без выдачи второго DEK.
Границы применения
При несовпадающем UUID методы изменения могут вернуть null. Не выводите старый документ целиком в журнал. Обновляйте конфигурацию клиентов до удаления старого имени.
Полная самостоятельная программа и архив
<?php
declare(strict_types=1);
use MongoDB\BSON\Binary;
use MongoDB\Driver\{ClientEncryption, Command, Manager, ReadPreference, Server};
use MongoDB\Driver\Exception\EncryptionException;
require __DIR__.'/kv132-helper.php';
// Keep driver messages, arguments and key documents outside public output.
set_exception_handler(static function(Throwable $error): void {
fwrite(STDERR, 'Vault example failed: '.$error::class.PHP_EOL);
exit(1);
});
if (!in_array(strtolower((string)ini_get('mongodb.debug')), ['', '0', 'off', 'no', 'false'], true)) {
throw new RuntimeException('Disable MongoDB debug tracing for this lab');
}
$settings = kv132Settings();
$uri = $settings['uri'];
$db = $settings['database'];
$manager = new Manager($uri, ['serverSelectionTimeoutMS'=>1500, 'connectTimeoutMS'=>500, 'socketTimeoutMS'=>3000]);
$server = $manager->selectServer(new ReadPreference(ReadPreference::PRIMARY));
if ($server->getType() !== Server::TYPE_STANDALONE) {
throw new RuntimeException('Use a dedicated standalone server');
}
$runtime = ['php'=>PHP_VERSION,'extension'=>phpversion('mongodb'),
'server'=>kv132Command($manager,$db,['buildInfo'=>1])[0]['version'],
'execution'=>'database','observed_host'=>$server->getHost(),'observed_port'=>$server->getPort()];
$owned = [];
try {
$vault = kv132Create($manager, $db, $owned);
$vaultName = $owned[0];
kv132Command($manager,$db,['createIndexes'=>$vaultName, 'indexes'=>[[
'name'=>'unique_alt_names', 'key'=>['keyAltNames'=>1], 'unique'=>true,
'partialFilterExpression'=>['keyAltNames'=>['$exists'=>true]],
]]],true);
$master = new Binary(random_bytes(96), Binary::TYPE_GENERIC);
$provider = ['local'=>['key'=>$master]];
$ce = $manager->createClientEncryption(['keyVaultNamespace'=>$vault, 'kmsProviders'=>$provider]);
$id = $ce->createDataKey('local', ['keyAltNames'=>['dispatch-v1']]);
$beforeAdd = $ce->addKeyAltName($id,'dispatch-v2');
$oldDuring = $ce->getKeyByAltName('dispatch-v1');
$newDuring = $ce->getKeyByAltName('dispatch-v2');
$beforeRemove = $ce->removeKeyAltName($id,'dispatch-v1');
$after = $ce->getKey($id);
$checks = ['old_returned_on_add'=>kv132AliasList($beforeAdd) === ['dispatch-v1'],
'both_during'=>$oldDuring->_id == $newDuring->_id, 'same_dek'=>$newDuring->_id == $id,
'old_returned_on_remove'=>kv132AliasList($beforeRemove) === ['dispatch-v1','dispatch-v2'],
'old_missing'=>$ce->getKeyByAltName('dispatch-v1') === null,
'only_new_name'=>kv132AliasList($after) === ['dispatch-v2']];
$rows = [['До добавления',implode(', ',kv132AliasList($beforeAdd))],
['В переходный период',implode(', ',kv132AliasList($beforeRemove))],['После перехода',implode(', ',kv132AliasList($after))],
['UUID сохранился',$after->_id == $id]];
} finally {
foreach (array_reverse($owned) as $collection) {
kv132Command($manager,$db,['drop'=>$collection],true);
}
}
$removed = true;
foreach ($owned as $collection) {
if (kv132Exists($manager,$db,$collection)) {$removed = false;}
}
$checks['owned_collections_removed'] = $removed && count($owned) === 1;
kv132Report('mongodb132-vault-alias-rename', ['Что проверили', 'Результат'], $rows, $checks, $runtime);
echo 'mongodb132-vault-alias-rename: OK', PHP_EOL;
Ожидаемый результат
mongodb132-vault-alias-rename: OK
Работа с собственным временным хранилищем и локальным учебным CMK. На страницу попадают только разрешённые поля и результаты сравнений.
Открыть фактическую таблицу · Данные результата в JSON
Упражнение. Снимите и новое имя. Убедитесь, что найти документ по UUID по-прежнему можно, а поиск по обоим именам больше ничего не вернёт.
Отказать во второй привязке имени и сохранить владельца
Склад и бухгалтерия независимо выбрали имя packing. Приложению нельзя молча направить новое шифрование к чужому ключу. Пусть MongoDB защитит уникальность, затем прочитаем, кто остался владельцем.
- Ключ склада получает packing, бухгалтерский — billing
- Попытка добавить packing второму ключу входит в конфликт
- Уникальный индекс отклоняет изменение
- Повторное чтение проверяет прежнего владельца
Короткий вызов
$owner = $ce->getKeyByAltName('packing');
if ($owner !== null) {
$alreadyBelongsHere = $owner->_id == $billing;
}Результат запуска
| Что проверили | Результат |
|---|---|
| Второе назначение отклонено | true |
| packing остался у первого ключа | true |
| Имя второго ключа | billing |
| Индекс уникален | true |
Почему получился такой результат
Полный файл создаёт unique-индекс по keyAltNames с partialFilterExpression для документов, где это поле существует. Безымянные документы не попадают в такой индекс. После отказа проверяем и оба ключа, и реальные параметры индекса через listIndexes.
Границы применения
Предварительный поиск имени удобен для подсказки, но два клиента могут увидеть свободное имя одновременно. Уникальность обеспечивает индекс, а приложение обрабатывает отказ.
Полная самостоятельная программа и архив
<?php
declare(strict_types=1);
use MongoDB\BSON\Binary;
use MongoDB\Driver\{ClientEncryption, Command, Manager, ReadPreference, Server};
use MongoDB\Driver\Exception\EncryptionException;
require __DIR__.'/kv132-helper.php';
// Keep driver messages, arguments and key documents outside public output.
set_exception_handler(static function(Throwable $error): void {
fwrite(STDERR, 'Vault example failed: '.$error::class.PHP_EOL);
exit(1);
});
if (!in_array(strtolower((string)ini_get('mongodb.debug')), ['', '0', 'off', 'no', 'false'], true)) {
throw new RuntimeException('Disable MongoDB debug tracing for this lab');
}
$settings = kv132Settings();
$uri = $settings['uri'];
$db = $settings['database'];
$manager = new Manager($uri, ['serverSelectionTimeoutMS'=>1500, 'connectTimeoutMS'=>500, 'socketTimeoutMS'=>3000]);
$server = $manager->selectServer(new ReadPreference(ReadPreference::PRIMARY));
if ($server->getType() !== Server::TYPE_STANDALONE) {
throw new RuntimeException('Use a dedicated standalone server');
}
$runtime = ['php'=>PHP_VERSION,'extension'=>phpversion('mongodb'),
'server'=>kv132Command($manager,$db,['buildInfo'=>1])[0]['version'],
'execution'=>'database','observed_host'=>$server->getHost(),'observed_port'=>$server->getPort()];
$owned = [];
try {
$vault = kv132Create($manager, $db, $owned);
$vaultName = $owned[0];
kv132Command($manager,$db,['createIndexes'=>$vaultName, 'indexes'=>[[
'name'=>'unique_alt_names', 'key'=>['keyAltNames'=>1], 'unique'=>true,
'partialFilterExpression'=>['keyAltNames'=>['$exists'=>true]],
]]],true);
$master = new Binary(random_bytes(96), Binary::TYPE_GENERIC);
$provider = ['local'=>['key'=>$master]];
$ce = $manager->createClientEncryption(['keyVaultNamespace'=>$vault, 'kmsProviders'=>$provider]);
$packing = $ce->createDataKey('local', ['keyAltNames'=>['packing']]);
$billing = $ce->createDataKey('local', ['keyAltNames'=>['billing']]);
$duplicate = false;
try {
$ce->addKeyAltName($billing,'packing');
} catch (MongoDB\Driver\Exception\RuntimeException $error) {
if ($error->getCode() !== 11000 && !str_contains($error->getMessage(),'E11000')) {
throw $error;
}
$duplicate = true;
}
$holder = $ce->getKeyByAltName('packing');
$bill = $ce->getKey($billing);
$indexes = kv132Command($manager,$db,['listIndexes'=>$vaultName]);
$altIndex = array_values(array_filter($indexes,fn($i)=>$i['name'] === 'unique_alt_names'))[0];
$checks = ['duplicate_rejected'=>$duplicate,'owner_retained'=>$holder->_id == $packing,
'second_key_unmodified'=>kv132AliasList($bill) === ['billing'], 'two_vault_keys'=>iterator_count($ce->getKeys()) === 2,
'unique_index'=>$altIndex['unique'] === true,
'only_named_documents'=>$altIndex['partialFilterExpression']['keyAltNames']['$exists'] === true];
$rows = [['Второе назначение отклонено',$duplicate],['packing остался у первого ключа',$holder->_id == $packing],
['Имя второго ключа',implode(', ',kv132AliasList($bill))],['Индекс уникален',$altIndex['unique']]];
} finally {
foreach (array_reverse($owned) as $collection) {
kv132Command($manager,$db,['drop'=>$collection],true);
}
}
$removed = true;
foreach ($owned as $collection) {
if (kv132Exists($manager,$db,$collection)) {$removed = false;}
}
$checks['owned_collections_removed'] = $removed && count($owned) === 1;
kv132Report('mongodb132-vault-unique-names', ['Что проверили', 'Результат'], $rows, $checks, $runtime);
echo 'mongodb132-vault-unique-names: OK', PHP_EOL;
Ожидаемый результат
mongodb132-vault-unique-names: OK
Работа с собственным временным хранилищем и локальным учебным CMK. На страницу попадают только разрешённые поля и результаты сравнений.
Открыть фактическую таблицу · Данные результата в JSON
Упражнение. Добавьте второму ключу свободное имя billing-next. По двум именам оба запроса должны теперь вернуть его один UUID.
Отключить поиск по последнему имени, сохранив сам ключ
Приложение снимает устаревший псевдоним. Оператор хочет знать, останется ли доступен ранее выданный талон. Сохраним зашифрованное учебное значение в памяти и проверим его новым клиентом.
- Создать DEK с именем legacy-note
- Зашифровать учебное поле
- Снять последнее имя и найти ключ по UUID
- Свежим клиентом с тем же CMK проверить чтение сохранённого поля
Короткий вызов
$previous = $ce->removeKeyAltName($id, 'legacy-note');
$byName = $ce->getKeyByAltName('legacy-note'); // null
$byId = $ce->getKey($id); // документ DEKРезультат запуска
| Что проверили | Результат |
|---|---|
| Снятое имя | legacy-note |
| Имён осталось | 0 |
| Доступен по UUID | true |
| Ранее зашифрованное поле читается | true |
Почему получился такой результат
Имя помогает приложению выбрать ключ при шифровании. Уже созданное значение связано с UUID DEK. Поэтому прежнее имя перестало находиться, но документ ключа остался, и свежий клиент открыл значение.
Границы применения
Снятие псевдонима не закрывает доступ к ранее зашифрованным полям. Когда требуется именно отзыв возможности читать данные, нужны отдельная политика доступа и учёт кэшей.
Полная самостоятельная программа и архив
<?php
declare(strict_types=1);
use MongoDB\BSON\Binary;
use MongoDB\Driver\{ClientEncryption, Command, Manager, ReadPreference, Server};
use MongoDB\Driver\Exception\EncryptionException;
require __DIR__.'/kv132-helper.php';
// Keep driver messages, arguments and key documents outside public output.
set_exception_handler(static function(Throwable $error): void {
fwrite(STDERR, 'Vault example failed: '.$error::class.PHP_EOL);
exit(1);
});
if (!in_array(strtolower((string)ini_get('mongodb.debug')), ['', '0', 'off', 'no', 'false'], true)) {
throw new RuntimeException('Disable MongoDB debug tracing for this lab');
}
$settings = kv132Settings();
$uri = $settings['uri'];
$db = $settings['database'];
$manager = new Manager($uri, ['serverSelectionTimeoutMS'=>1500, 'connectTimeoutMS'=>500, 'socketTimeoutMS'=>3000]);
$server = $manager->selectServer(new ReadPreference(ReadPreference::PRIMARY));
if ($server->getType() !== Server::TYPE_STANDALONE) {
throw new RuntimeException('Use a dedicated standalone server');
}
$runtime = ['php'=>PHP_VERSION,'extension'=>phpversion('mongodb'),
'server'=>kv132Command($manager,$db,['buildInfo'=>1])[0]['version'],
'execution'=>'database','observed_host'=>$server->getHost(),'observed_port'=>$server->getPort()];
$owned = [];
try {
$vault = kv132Create($manager, $db, $owned);
$vaultName = $owned[0];
kv132Command($manager,$db,['createIndexes'=>$vaultName, 'indexes'=>[[
'name'=>'unique_alt_names', 'key'=>['keyAltNames'=>1], 'unique'=>true,
'partialFilterExpression'=>['keyAltNames'=>['$exists'=>true]],
]]],true);
$master = new Binary(random_bytes(96), Binary::TYPE_GENERIC);
$provider = ['local'=>['key'=>$master]];
$ce = $manager->createClientEncryption(['keyVaultNamespace'=>$vault, 'kmsProviders'=>$provider]);
$id = $ce->createDataKey('local', ['keyAltNames'=>['legacy-note']]);
$protected = kv132Cipher($ce,$id);
$previous = $ce->removeKeyAltName($id,'legacy-note');
$now = $ce->getKey($id);
$byName = $ce->getKeyByAltName('legacy-note');
$fresh = kv132Fresh($uri,$vault,$master);
$opened = $fresh->decrypt($protected) === 'учебный талон R-7';
$checks = ['was_named'=>kv132AliasList($previous) === ['legacy-note'], 'lookup_empty'=>$byName === null,
'still_present'=>$now->_id == $id,'no_names_left'=>kv132AliasList($now) === [],
'same_one_key'=>iterator_count($fresh->getKeys()) === 1,'fresh_decryption'=>$opened];
$rows = [['Снятое имя',implode(', ',kv132AliasList($previous))],['Имён осталось',count(kv132AliasList($now))],
['Доступен по UUID',$now->_id == $id],['Ранее зашифрованное поле читается',$opened]];
} finally {
foreach (array_reverse($owned) as $collection) {
kv132Command($manager,$db,['drop'=>$collection],true);
}
}
$removed = true;
foreach ($owned as $collection) {
if (kv132Exists($manager,$db,$collection)) {$removed = false;}
}
$checks['owned_collections_removed'] = $removed && count($owned) === 1;
kv132Report('mongodb132-vault-last-name', ['Что проверили', 'Результат'], $rows, $checks, $runtime);
echo 'mongodb132-vault-last-name: OK', PHP_EOL;
Ожидаемый результат
mongodb132-vault-last-name: OK
Работа с собственным временным хранилищем и локальным учебным CMK. На страницу попадают только разрешённые поля и результаты сравнений.
Открыть фактическую таблицу · Данные результата в JSON
Упражнение. Попробуйте присвоить этому же ключу другое свободное имя. Проверьте чтение уже зашифрованного поля и поиск по новому имени.
Переупаковать один ключ и проверить ранее сохранённую запись
В vault два ключа. Обслуживающая задача должна переупаковать только ключ dispatch. Чтобы проверить влияние на клиентов, сначала сохраним талон R-7 в отдельной коллекции, затем прочитаем его заново.
- Записать учебный талон под ключом dispatch
- Передать в rewrapManyDataKey фильтр по одному UUID
- Сравнить идентификатор и содержимое контрольной записи
- Создать новый клиент и расшифровать сохранённое поле
Короткий вызов
$result = $ce->rewrapManyDataKey(['_id' => $id]);
$hadWrite = isset($result->bulkWriteResult);Результат запуска
| Что проверили | Результат |
|---|---|
| Есть результат записи по фильтру | true |
| UUID сохранился | true |
| Другой ключ остался прежним | true |
| Шифротекст записи остался прежним | true |
| Новый клиент прочитал старый талон | true |
Почему получился такой результат
Драйвер открывает DEK и заново оборачивает его. Проверяется изменение обёртки выбранного ключа, сохранение второй обёртки и исходного шифротекста талона. Проверки сравнивают объекты в памяти. Публичный результат содержит только логические итоги. Опции провайдера не изменены: используется тот же временный мастер-ключ.
Границы применения
Это переупаковка с прежним CMK. Для настоящей смены CMK заранее готовят доступ к прежнему и новому провайдеру, фильтр, резервные копии и проверку чтения. Пустой фильтр затронул бы всё хранилище.
Полная самостоятельная программа и архив
<?php
declare(strict_types=1);
use MongoDB\BSON\Binary;
use MongoDB\Driver\{ClientEncryption, Command, Manager, ReadPreference, Server};
use MongoDB\Driver\Exception\EncryptionException;
require __DIR__.'/kv132-helper.php';
// Keep driver messages, arguments and key documents outside public output.
set_exception_handler(static function(Throwable $error): void {
fwrite(STDERR, 'Vault example failed: '.$error::class.PHP_EOL);
exit(1);
});
if (!in_array(strtolower((string)ini_get('mongodb.debug')), ['', '0', 'off', 'no', 'false'], true)) {
throw new RuntimeException('Disable MongoDB debug tracing for this lab');
}
$settings = kv132Settings();
$uri = $settings['uri'];
$db = $settings['database'];
$manager = new Manager($uri, ['serverSelectionTimeoutMS'=>1500, 'connectTimeoutMS'=>500, 'socketTimeoutMS'=>3000]);
$server = $manager->selectServer(new ReadPreference(ReadPreference::PRIMARY));
if ($server->getType() !== Server::TYPE_STANDALONE) {
throw new RuntimeException('Use a dedicated standalone server');
}
$runtime = ['php'=>PHP_VERSION,'extension'=>phpversion('mongodb'),
'server'=>kv132Command($manager,$db,['buildInfo'=>1])[0]['version'],
'execution'=>'database','observed_host'=>$server->getHost(),'observed_port'=>$server->getPort()];
$owned = [];
try {
$vault = kv132Create($manager, $db, $owned);
$vaultName = $owned[0];
kv132Command($manager,$db,['createIndexes'=>$vaultName, 'indexes'=>[[
'name'=>'unique_alt_names', 'key'=>['keyAltNames'=>1], 'unique'=>true,
'partialFilterExpression'=>['keyAltNames'=>['$exists'=>true]],
]]],true);
$master = new Binary(random_bytes(96), Binary::TYPE_GENERIC);
$provider = ['local'=>['key'=>$master]];
$ce = $manager->createClientEncryption(['keyVaultNamespace'=>$vault, 'kmsProviders'=>$provider]);
$dataNs = kv132Create($manager,$db,$owned);
$id = $ce->createDataKey('local', ['keyAltNames'=>['dispatch']]);
$other = $ce->createDataKey('local', ['keyAltNames'=>['keep']]);
$oldEnvelope = $ce->getKey($id)->keyMaterial;
$otherEnvelope = $ce->getKey($other)->keyMaterial;
$protected = kv132Cipher($ce,$id);
kv132Insert($manager,$dataNs,[['_id'=>'R-7','protected'=>$protected]]);
$result = $ce->rewrapManyDataKey(['_id'=>$id]);
$after = $ce->getKey($id);
$stored = kv132Read($manager,$dataNs,['_id'=>'R-7'],['_id'=>0,'protected'=>1])[0]['protected'];
$fresh = kv132Fresh($uri,$vault,$master);
$opened = $fresh->decrypt($stored) === 'учебный талон R-7';
$noMatch = $ce->rewrapManyDataKey(['keyAltNames'=>'not-assigned']);
$checks = ['matched_result'=>isset($result->bulkWriteResult), 'uuid_preserved'=>$after->_id == $id,
'target_wrapped_again'=>$after->keyMaterial != $oldEnvelope,
'other_unchanged'=>$ce->getKey($other)->keyMaterial == $otherEnvelope,
'field_unchanged'=>$stored == $protected, 'cold_read'=>$opened,
'no_match_result'=>($noMatch->bulkWriteResult ?? null) === null];
$rows = [['Есть результат записи по фильтру',isset($result->bulkWriteResult)],['UUID сохранился',$after->_id == $id],
['Другой ключ остался прежним',$checks['other_unchanged']],['Шифротекст записи остался прежним',$stored == $protected],
['Новый клиент прочитал старый талон',$opened]];
} finally {
foreach (array_reverse($owned) as $collection) {
kv132Command($manager,$db,['drop'=>$collection],true);
}
}
$removed = true;
foreach ($owned as $collection) {
if (kv132Exists($manager,$db,$collection)) {$removed = false;}
}
$checks['owned_collections_removed'] = $removed && count($owned) === 2;
kv132Report('mongodb132-vault-rewrap-one', ['Что проверили', 'Результат'], $rows, $checks, $runtime);
echo 'mongodb132-vault-rewrap-one: OK', PHP_EOL;
Ожидаемый результат
mongodb132-vault-rewrap-one: OK
Работа с собственным временным хранилищем и локальным учебным CMK. На страницу попадают только разрешённые поля и результаты сравнений.
Открыть фактическую таблицу · Данные результата в JSON
Упражнение. Подставьте фильтр по несуществующему псевдониму. В этом успешном вызове нечего записывать; проверьте отсутствие bulkWriteResult.
При смене локального CMK автор смешивает настройки KMS с опциями операции переупаковки. Здесь измеряется переупаковка одного DEK при сохранении прежнего CMK. Rotation of a local master key на Stack Overflow.
Удалить учебный ключ и проверить доступность старого талона
В учебной среде закончился срок тестового ключа. Посмотрим отдельно на судьбу его документа и на судьбу сохранённого талона. Этот опыт касается только ключа, созданного текущим запуском.
- Сохранить R-7 и выполнить контрольное расшифрование
- Удалить DEK по его UUID
- Прочитать талон обычным менеджером
- Попробовать открыть поле новым клиентом, у которого нет сохранённого DEK
Короткий вызов
$receipt = $ce->deleteKey($id);
$number = $receipt->deletedCount;
$gone = $ce->getKey($id) === null;Результат запуска
| Что проверили | Результат |
|---|---|
| До удаления талон читается | true |
| Удалено документов DEK | 1 |
| Талон в бизнес-коллекции остался | true |
| Свежему клиенту не удалось открыть поле | true |
| Повторно удалено | 0 |
Почему получился такой результат
deleteKey вернул число удалённых ключевых документов. Заказ остался в другой коллекции с тем же зашифрованным полем. Свежий клиент уже не смог извлечь нужный ключ. Ещё одна попытка удалить тот же UUID дала ноль.
Границы применения
Ранее работавший клиент может некоторое время использовать закэшированный DEK. Проверка нового экземпляра подтверждает отсутствие материала в его свежем контексте. Копии ключа вне этой учебной коллекции, копии открытого текста и резервные копии имеют отдельный жизненный цикл.
Полная самостоятельная программа и архив
<?php
declare(strict_types=1);
use MongoDB\BSON\Binary;
use MongoDB\Driver\{ClientEncryption, Command, Manager, ReadPreference, Server};
use MongoDB\Driver\Exception\EncryptionException;
require __DIR__.'/kv132-helper.php';
// Keep driver messages, arguments and key documents outside public output.
set_exception_handler(static function(Throwable $error): void {
fwrite(STDERR, 'Vault example failed: '.$error::class.PHP_EOL);
exit(1);
});
if (!in_array(strtolower((string)ini_get('mongodb.debug')), ['', '0', 'off', 'no', 'false'], true)) {
throw new RuntimeException('Disable MongoDB debug tracing for this lab');
}
$settings = kv132Settings();
$uri = $settings['uri'];
$db = $settings['database'];
$manager = new Manager($uri, ['serverSelectionTimeoutMS'=>1500, 'connectTimeoutMS'=>500, 'socketTimeoutMS'=>3000]);
$server = $manager->selectServer(new ReadPreference(ReadPreference::PRIMARY));
if ($server->getType() !== Server::TYPE_STANDALONE) {
throw new RuntimeException('Use a dedicated standalone server');
}
$runtime = ['php'=>PHP_VERSION,'extension'=>phpversion('mongodb'),
'server'=>kv132Command($manager,$db,['buildInfo'=>1])[0]['version'],
'execution'=>'database','observed_host'=>$server->getHost(),'observed_port'=>$server->getPort()];
$owned = [];
try {
$vault = kv132Create($manager, $db, $owned);
$vaultName = $owned[0];
kv132Command($manager,$db,['createIndexes'=>$vaultName, 'indexes'=>[[
'name'=>'unique_alt_names', 'key'=>['keyAltNames'=>1], 'unique'=>true,
'partialFilterExpression'=>['keyAltNames'=>['$exists'=>true]],
]]],true);
$master = new Binary(random_bytes(96), Binary::TYPE_GENERIC);
$provider = ['local'=>['key'=>$master]];
$ce = $manager->createClientEncryption(['keyVaultNamespace'=>$vault, 'kmsProviders'=>$provider]);
$dataNs = kv132Create($manager,$db,$owned);
$id = $ce->createDataKey('local', ['keyAltNames'=>['training-only']]);
$protected = kv132Cipher($ce,$id);
kv132Insert($manager,$dataNs,[['_id'=>'R-7','protected'=>$protected]]);
$before = kv132Fresh($uri,$vault,$master)->decrypt($protected) === 'учебный талон R-7';
$result = $ce->deleteKey($id);
$missing = $ce->getKey($id) === null;
$docs = kv132Read($manager,$dataNs,['_id'=>'R-7'],['_id'=>0,'protected'=>1]);
$fresh = kv132Fresh($uri,$vault,$master);
$refused = false;
try {
$fresh->decrypt($docs[0]['protected']);
} catch (EncryptionException $error) {
$refused = true;
}
$again = $ce->deleteKey($id);
$checks = ['opened_before'=>$before, 'deleted_one'=>(int)$result->deletedCount === 1,
'id_missing'=>$missing,'business_document_remains'=>count($docs) === 1 && $docs[0]['protected'] == $protected,
'failed_on_fresh_client'=>$refused,'delete_again_zero'=>(int)$again->deletedCount === 0];
$rows = [['До удаления талон читается',$before],['Удалено документов DEK',(int)$result->deletedCount],
['Талон в бизнес-коллекции остался',count($docs) === 1],['Свежему клиенту не удалось открыть поле',$refused],
['Повторно удалено',(int)$again->deletedCount]];
} finally {
foreach (array_reverse($owned) as $collection) {
kv132Command($manager,$db,['drop'=>$collection],true);
}
}
$removed = true;
foreach ($owned as $collection) {
if (kv132Exists($manager,$db,$collection)) {$removed = false;}
}
$checks['owned_collections_removed'] = $removed && count($owned) === 2;
kv132Report('mongodb132-vault-retire-test-key', ['Что проверили', 'Результат'], $rows, $checks, $runtime);
echo 'mongodb132-vault-retire-test-key: OK', PHP_EOL;
Ожидаемый результат
mongodb132-vault-retire-test-key: OK
Работа с собственным временным хранилищем и локальным учебным CMK. На страницу попадают только разрешённые поля и результаты сравнений.
Открыть фактическую таблицу · Данные результата в JSON
Упражнение. Сравните этот результат со снятием последнего псевдонима. Какой из двух сценариев быстрее вернёт доступ старому приложению, если оператор ошибся?
Класс и методы обслуживания хранилища
| Класс или метод | Задача |
|---|---|
| MongoDB\Driver\ClientEncryption | Получить клиентский инструмент для выдачи DEK и работы с зашифрованным значением. |
| MongoDB\Driver\Manager::createClientEncryption | Создать ClientEncryption с текущим Manager для маршрутизации запросов к ключам. |
| MongoDB\Driver\ClientEncryption::__construct | Явно связать объект с клиентом vault и локальным провайдером. |
| MongoDB\Driver\ClientEncryption::createDataKey | Создать документ DEK и получить UUID для последующих операций. |
| MongoDB\Driver\ClientEncryption::getKey | Проверить конкретный ключевой документ по UUID. |
| MongoDB\Driver\ClientEncryption::getKeyByAltName | Найти ключ по понятному имени из конфигурации. |
| MongoDB\Driver\ClientEncryption::getKeys | Обойти документы ключей в данном vault. |
| MongoDB\Driver\ClientEncryption::addKeyAltName | Добавить альтернативное имя к существующему DEK. |
| MongoDB\Driver\ClientEncryption::removeKeyAltName | Снять имя с ключа после миграции клиента. |
| MongoDB\Driver\ClientEncryption::rewrapManyDataKey | Повторно зашифровать обёртку выбранных ключей данных. |
| MongoDB\Driver\ClientEncryption::deleteKey | Удалить один ключевой документ по UUID. |
Применить ключ к полю заказа
Для новой роли выдайте один DEK, для нового имени добавьте псевдоним к существующему UUID. Перед обслуживанием ограничьте фильтр и запишите ожидаемый эффект. После операции проверьте, как её результат виден свежему клиенту, читающему сохранённое поле.
Перейти к защите поля заказа и результату для читателя · Проверить конфигурацию PHP и MongoDB.
Функции и методы этой темы
Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.
MongoDB\Driver\ClientEncryptionПолучить клиентский инструмент для выдачи DEK и работы с зашифрованным значением.
ext-mongodb 1.7.0 и новее · mongodb
final class MongoDB\Driver\ClientEncryptionПараметры
Объект связывает MongoDB key vault, KMS-провайдеры и контекст libmongocrypt. Его можно получить фабрикой Manager или создать явно, передав настройки.
Возвращаемое значение
Методы управления работают с UUID и служебными документами. encrypt возвращает BSON Binary, decrypt — исходное значение. Поддерживаемые константы и отдельный API для диапазонного QE разобраны в обзоре ниже.
Ошибки и ограничения
Для работы нужен драйвер с поддержкой libmongocrypt. При недоступном сервере, неверных аргументах, ошибках авторизации и расшифрования возможны различные исключения.
Версии
ext-mongodb 1.7.0 и новее
Выдать ключ для приложения
$id = $ce->createDataKey('local', ['keyAltNames'=>['pickup-code']]);Результат UUID пригоден для последующих операций с документом ключа.
Полная программа и проверенные значения
Проверить применение ключа после смены конфигурации
$fresh = kv132Fresh($uri, $vault, $master);
$ok = $fresh->decrypt($protected) === 'учебный талон R-7';Независимый объект действительно получает DEK для чтения. На страницу выводится только результат сравнения.
MongoDB\Driver\Manager::createClientEncryptionСоздать ClientEncryption с текущим Manager для маршрутизации запросов к ключам.
ext-mongodb 1.7.0 и новее · mongodb
final public function MongoDB\Driver\Manager::createClientEncryption(array $options): MongoDB\Driver\ClientEncryptionПараметры
Задайте keyVaultNamespace и kmsProviders. По умолчанию используется этот Manager. Необязательный keyVaultClient позволяет маршрутизировать операции vault через другой Manager. tlsOptions задаёт настройки для облачных провайдеров. Таблица настройки KMS находится в этой главе.
Возвращаемое значение
Возвращает новый объект ClientEncryption. Изготовление этого объекта ещё не создаёт DEK и не шифрует документы.
Ошибки и ограничения
Ошибки разбора настроек приводят к InvalidArgumentException. При отсутствии поддержки libmongocrypt возможен RuntimeException. Сбой последующего доступа к vault следует отличать от сбоя создания объекта.
Версии
ext-mongodb 1.7.0 и новее
Подключить собственную коллекцию ключей
$ce = $manager->createClientEncryption([
'keyVaultNamespace'=>$vault, 'kmsProviders'=>$provider,
]);В новой коллекции getKeys пока не возвращает записей. Первый DEK создаётся отдельным вызовом.
Полная программа и проверенные значения
Повторно открыть vault компонента возвратов
$ce = $manager->createClientEncryption($options);
$doc = $ce->getKeyByAltName('returns');Фабрика выбирает текущий Manager. По имени возвращается тот же UUID, что и при обращении явно настроенного компонента.
MongoDB\Driver\ClientEncryption::__constructЯвно связать объект с клиентом vault и локальным провайдером.
ext-mongodb 1.14.0 и новее · mongodb
final public function MongoDB\Driver\ClientEncryption::__construct(array $options)Параметры
В отличие от фабрики, параметр keyVaultClient внутри options обязателен. Укажите Manager, имя вида database.collection и хотя бы один KMS в kmsProviders. Для local подходят 96 байт как Binary либо их Base64-представление.
Возвращаемое значение
Выражение new возвращает новый контекст ClientEncryption. Настроенный объект может искать ключевые документы, шифровать и расшифровывать значения.
Ошибки и ограничения
Неверная конфигурация либо сборка без libmongocrypt не позволяют нормально использовать контекст. В 1.14 появился публичный конструктор. Объект с тем же namespace, но иным CMK может находить документы, не имея возможности открывать поля.
Версии
ext-mongodb 1.14.0 и новее
Передать Manager конструктору
$direct = new MongoDB\Driver\ClientEncryption($options + ['keyVaultClient'=>$manager]);Компонент видит ключ returns в указанной коллекции.
Полная программа и проверенные значения
Исключить влияние прогретого кэша
$fresh = kv132Fresh($uri, $vault, $master);Помощник создаёт отдельный Manager с disableClientPersistence и прямой ClientEncryption. Его попытка расшифровать значение после удаления ключа завершается ошибкой.
MongoDB\Driver\ClientEncryption::createDataKeyСоздать документ DEK и получить UUID для последующих операций.
ext-mongodb 1.7.0 и новее · mongodb
final public function MongoDB\Driver\ClientEncryption::createDataKey(string $kmsProvider, ?array $options = null): MongoDB\BSON\BinaryПараметры
Строка kmsProvider указывает настроенный провайдер. В options можно передать keyAltNames — список строк. Для внешних провайдеров укажите masterKey с идентификатором CMK по правилам выбранного KMS. local берёт свои 96 байт из настроек клиента. Необязательный keyMaterial (с 1.15) — 96 байт Binary для самого создаваемого DEK; по умолчанию драйвер сгенерирует их сам.
Возвращаемое значение
Возвращает BSON Binary подтипа 4 с идентификатором нового документа. Материал DEK остаётся обёрнутым в vault.
Ошибки и ограничения
Дублирование имени блокирует уникальный индекс. Неверные параметры либо отказ KMS или MongoDB должны обрабатываться на этапе выпуска. Провайдеры Azure/GCP добавлены в 1.10, опция delegated у KMIP — в 1.20.
Версии
ext-mongodb 1.7.0 и новее
Создать именованный ключ выдачи
$id = $ce->createDataKey('local', ['keyAltNames'=>['pickup-code']]);Приложение может сразу найти новый документ по понятной метке.
Полная программа и проверенные значения
Создать безымянный ключ для обращения по UUID
$unlabelled = $ce->createDataKey('local');
$doc = $ce->getKey($unlabelled);Входит в общий список DEK, а список имён у него пуст.
MongoDB\Driver\ClientEncryption::getKeyПроверить конкретный ключевой документ по UUID.
ext-mongodb 1.15.0 и новее · mongodb
final public function MongoDB\Driver\ClientEncryption::getKey(MongoDB\BSON\Binary $keyId): ?objectПараметры
Аргументом служит BSON Binary с подтипом 4, полученный при выдаче DEK. Строка UUID сама по себе не соответствует этой сигнатуре.
Возвращаемое значение
Возвращает документ текущего состояния или null при отсутствии совпадения. Извлекайте только нужные поля.
Ошибки и ограничения
При невалидном идентификаторе возможен InvalidArgumentException. Отказ связи или авторизации отличается от успешного запроса, вернувшего null.
Версии
ext-mongodb 1.15.0 и новее
Проверить выдачу ключа
$doc = $ce->getKey($id);
$ok = $doc !== null && $doc->_id == $id;UUID в сохранённом документе совпадает с UUID, возвращённым фабрикой ключа.
Полная программа и проверенные значения
Проверить отсутствие после удаления
$gone = $ce->getKey($id) === null;Показывает состояние коллекции. Возможность открыть старое поле проверяют отдельно на свежем контексте.
MongoDB\Driver\ClientEncryption::getKeyByAltNameНайти ключ по понятному имени из конфигурации.
ext-mongodb 1.15.0 и новее · mongodb
final public function MongoDB\Driver\ClientEncryption::getKeyByAltName(string $keyAltName): ?objectПараметры
Передайте строковое альтернативное имя. Задайте индекс уникальности по keyAltNames для документов, где это поле существует.
Возвращаемое значение
При совпадении возвращает документ, иначе null. Это метод клиента расширения. Команда сервера с именем getKeyByAltName не требуется.
Ошибки и ограничения
Проверяйте null перед чтением поля. Полученный документ содержит служебные данные. Исключения доступа отделяйте от отсутствия записи.
Версии
ext-mongodb 1.15.0 и новее
Проверить настройку роли returns
$doc = $direct->getKeyByAltName('returns');
$found = $doc !== null;Можно показать в интерфейсе состояние настройки, не передавая всю запись.
Полная программа и проверенные значения
Проверить переход на dispatch-v2
$owner = $ce->getKeyByAltName('dispatch-v2');
$old = $ce->getKeyByAltName('dispatch-v1');После снятия старой метки первый поиск находит прежний UUID, второй даёт null.
MongoDB\Driver\ClientEncryption::getKeysОбойти документы ключей в данном vault.
ext-mongodb 1.15.0 и новее · mongodb
final public function MongoDB\Driver\ClientEncryption::getKeys(): MongoDB\Driver\CursorПараметры
Аргументов нет. Элементы берутся из keyVaultNamespace указанного при создании объекта.
Возвращаемое значение
Возвращает Cursor. Нулевое число элементов нормально для нового хранилища.
Ошибки и ограничения
Метод читает все документы. Не сериализуйте весь курсор в журнал или HTTP-ответ. Разрывы соединения возможны во время обхода.
Версии
ext-mongodb 1.15.0 и новее
Проверить пустое хранилище до инициализации
$empty = iterator_count($ce->getKeys()) === 0;Свежий курсор потребляется при подсчёте. Новую проверку выполните по новому курсору.
Полная программа и проверенные значения
Собрать разрешённые поля для списка
foreach ($ce->getKeys() as $doc) {
$names = kv132AliasList($doc);
// Использовать только имена.
}Из трёх ключей получены два имени и один безымянный документ.
MongoDB\Driver\ClientEncryption::addKeyAltNameДобавить альтернативное имя к существующему DEK.
ext-mongodb 1.15.0 и новее · mongodb
final public function MongoDB\Driver\ClientEncryption::addKeyAltName(MongoDB\BSON\Binary $keyId, string $keyAltName): ?objectПараметры
keyId — BSON UUID подтипа 4, keyAltName — желаемая строковая метка. MongoDB хранит набор имён ключа.
Возвращаемое значение
Возвращается прежняя версия документа или null при отсутствии документа с этим UUID. Запросите getKey снова, чтобы увидеть новое состояние.
Ошибки и ограничения
Попытка занять чужую метку приводит к конфликту уникального индекса. Конфликт обработайте как отказ операции, затем проверьте владельца. Проблемы подключения и авторизации имеют другие причины.
Версии
ext-mongodb 1.15.0 и новее
Добавить новое имя перед обновлением приложения
$previous = $ce->addKeyAltName($id,'dispatch-v2');Возвращённый документ ещё содержит только исходное имя. Отдельный поиск уже видит оба.
Полная программа и проверенные значения
Проверить попытку занять чужое имя
$ce->addKeyAltName($billing, 'packing');В полном файле вызов находится в обработчике отказа. Второй ключ остался с именем billing.
MongoDB\Driver\ClientEncryption::removeKeyAltNameСнять имя с ключа после миграции клиента.
ext-mongodb 1.15.0 и новее · mongodb
final public function MongoDB\Driver\ClientEncryption::removeKeyAltName(MongoDB\BSON\Binary $keyId, string $keyAltName): ?objectПараметры
Передайте UUID конкретного ключа и метку, которую снимаете. Сохраняются остальные метки этого DEK.
Возвращаемое значение
Метод возвращает прежний документ или null при отсутствии совпадающего ключа. Нынешние имена проверяются отдельным чтением.
Ошибки и ограничения
Снятие последнего имени не удаляет сам ключ. При ошибке связи проверьте фактическое состояние перед повтором.
Версии
ext-mongodb 1.15.0 и новее
Завершить миграцию после обновления клиентов
$previous = $ce->removeKeyAltName($id,'dispatch-v1');
$current = $ce->getKey($id);До вызова доступны два имени, после остаётся dispatch-v2.
Полная программа и проверенные значения
Снять последнюю метку
$ce->removeKeyAltName($id,'legacy-note');
$stillPresent = $ce->getKey($id) !== null;UUID остаётся действительным, и свежий клиент расшифровывает ранее созданный учебный талон.
MongoDB\Driver\ClientEncryption::rewrapManyDataKeyПовторно зашифровать обёртку выбранных ключей данных.
ext-mongodb 1.15.0 и новее · mongodb
final public function MongoDB\Driver\ClientEncryption::rewrapManyDataKey(array|object $filter, ?array $options = null): objectПараметры
filter задаёт BSON-предикат. Для одного DEK возьмите его _id как UUID. Пустой фильтр соответствует всем ключам. Если provider в options отсутствует, каждый выбранный ключ использует текущий KMS. При переходе к другому провайдеру provider и его masterKey должны соответствовать требованиям; у local поле masterKey не требуется.
Возвращаемое значение
Результат — объект с необязательным bulkWriteResult. Отсутствие совпадений или неподтверждённая запись не дают подтверждённого bulk-результата.
Ошибки и ограничения
Операции чтения, открывания и повторного сохранения могут завершиться ошибкой доступа, конфигурации KMS или EncryptionException. Для проверки читайте прежнюю бизнес-запись свежим клиентом.
Версии
ext-mongodb 1.15.0 и новее
Обслужить один DEK и оставить соседний прежним
$result = $ce->rewrapManyDataKey(['_id'=>$id]);
$written = isset($result->bulkWriteResult);Проверяем UUID и обёртку именно выбранного ключа, второй ключ служит контролем.
Полная программа и проверенные значения
Обработать пустую выборку для операции обслуживания
$none = $ce->rewrapManyDataKey(['keyAltNames'=>'not-assigned']);
$didWrite = isset($none->bulkWriteResult);Для неизвестной роли внутренней записи не потребовалось. Скрипт отличает такой исход от ошибки доступа.
MongoDB\Driver\ClientEncryption::deleteKeyУдалить один ключевой документ по UUID.
ext-mongodb 1.15.0 и новее · mongodb
final public function MongoDB\Driver\ClientEncryption::deleteKey(MongoDB\BSON\Binary $keyId): objectПараметры
Метод принимает Binary с подтипом 4. Используйте его только для конкретного ключа, который действительно подлежит удалению.
Возвращаемое значение
Возвращается объект результата внутреннего deleteOne. В проверяемой версии поле deletedCount показывает число удалённых документов.
Ошибки и ограничения
Сохранённые бизнес-документы продолжают содержать шифротексты. У клиентов могут сохраняться кэшированные DEK. Исключения связи и авторизации нельзя выдавать за удаление нуля документов.
Версии
ext-mongodb 1.15.0 и новее
Проверить первое удаление временного ключа
$deleted = $ce->deleteKey($id)->deletedCount;Результат первого удаления равен одному. Свежий клиент больше не расшифровывает учебный талон.
Полная программа и проверенные значения
Не посчитать повторное удаление как успешную новую работу
$again = $ce->deleteKey($id)->deletedCount;Второй успешный вызов по прежнему UUID возвращает ноль. Данных с таким id уже нет.
Источники и обсуждения
- ClientEncryption
- Manager::createClientEncryption
- ClientEncryption::__construct
- ClientEncryption::createDataKey
- ClientEncryption::getKey
- ClientEncryption::getKeyByAltName
- ClientEncryption::getKeys
- ClientEncryption::addKeyAltName
- ClientEncryption::removeKeyAltName
- ClientEncryption::rewrapManyDataKey
- ClientEncryption::deleteKey
- MongoDB 8.0. Управление ключами CSFLE
- MongoDB. Явное шифрование
- MongoDB. Чтение после удаления ключа
Код и руководство с собой
Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.
Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.
Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


