Перейти к содержимому
Поиск
Php Разработка

PHP: полный мануал с примерами, функциями и ООП

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

Александр НекрасовИзменено:
Практический мануал PHP: от переменных и функций к объектной модели интернет-магазина
PHP 8.5.11468 глав6256 карточек справочника
Дерево руководства

Глава 176 · После подключения к MongoDB; криптографические роли вводятся по шагам

MongoDB: выдать, найти и обслужить ключи для шифрования полей

Схема CMK → DEK → поле, выпуск ключа, точечный поиск, защита уникальности имён, переупаковка одного документа и холодная проверка удаления.

От кода получения к ключу с понятным именем

На складе вводят коды получения. Приложение должно находить свой ключ по имени pickup-code, даже когда обновляется один из его компонентов. Сначала создадим один ключ и прочитаем у него только имя. Затем проверим, что происходит с уже сохранённым талоном при изменении имён и обслуживании ключей.

Как появляется ключ для поля заказа
  1. PHP создаёт 96 случайных байт для локального учебного CMK
  2. Драйвер создаёт DEK и защищает его этим CMK
  3. В vault сохраняется ключевой документ с UUID и меткой pickup-code
  4. 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.keyCMK как 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 для диапазонов.

Почему имя ключа должно быть уникальным

Как индекс защищает псевдонимы
  1. Для vault создать индекс keyAltNames: 1
  2. Включить только документы с этим полем
  3. MongoDB допускает имя packing у одного DEK
  4. Второму ключу остаётся его собственное имя 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.

Как меняется состояние
  1. Создать пустое хранилище и индекс имён
  2. Драйвер создаёт DEK и упаковывает его с помощью локального CMK
  3. MongoDB сохраняет документ ключа
  4. 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 явно. Проверим, с каким хранилищем они работают.

Как меняется состояние
  1. Фабрика использует текущий Manager
  2. В прямой конструктор передаём keyVaultClient
  3. Оба клиента читают указанную vault-коллекцию
  4. Сравниваем UUID при поиске по ID и имени

Короткий вызов

$factory = $manager->createClientEncryption($options);
$direct = new MongoDB\Driver\ClientEncryption($options + ['keyVaultClient' => $manager]);

Результат запуска

Что проверилиРезультат
Созданы разные объекты PHPtrue
Обе ссылки нашли один UUIDtrue
Ключ 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.

Составить список ролей, не выгружая содержимое ключей

Поддержка хочет знать, есть ли ключи для выдачи и для возвратов. Выведем алфавитный список учебных ролей и отдельно число ключей, которых никто не назвал.

Как меняется состояние
  1. Создать три DEK, один без имени
  2. Получить Cursor хранилища
  3. Для каждой строки собрать только имена
  4. Посчитать безымянные документы и отсортировать роли

Короткий вызов

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.

Как меняется состояние
  1. Создать dispatch-v1
  2. Добавить dispatch-v2 и проверить оба имени
  3. После перехода снять старое имя
  4. Сверить 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 защитит уникальность, затем прочитаем, кто остался владельцем.

Как меняется состояние
  1. Ключ склада получает packing, бухгалтерский — billing
  2. Попытка добавить packing второму ключу входит в конфликт
  3. Уникальный индекс отклоняет изменение
  4. Повторное чтение проверяет прежнего владельца

Короткий вызов

$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.

Отключить поиск по последнему имени, сохранив сам ключ

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

Как меняется состояние
  1. Создать DEK с именем legacy-note
  2. Зашифровать учебное поле
  3. Снять последнее имя и найти ключ по UUID
  4. Свежим клиентом с тем же CMK проверить чтение сохранённого поля

Короткий вызов

$previous = $ce->removeKeyAltName($id, 'legacy-note');
$byName = $ce->getKeyByAltName('legacy-note'); // null
$byId = $ce->getKey($id);                    // документ DEK

Результат запуска

Что проверилиРезультат
Снятое имяlegacy-note
Имён осталось0
Доступен по UUIDtrue
Ранее зашифрованное поле читается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 в отдельной коллекции, затем прочитаем его заново.

Как меняется состояние
  1. Записать учебный талон под ключом dispatch
  2. Передать в rewrapManyDataKey фильтр по одному UUID
  3. Сравнить идентификатор и содержимое контрольной записи
  4. Создать новый клиент и расшифровать сохранённое поле

Короткий вызов

$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.

Удалить учебный ключ и проверить доступность старого талона

В учебной среде закончился срок тестового ключа. Посмотрим отдельно на судьбу его документа и на судьбу сохранённого талона. Этот опыт касается только ключа, созданного текущим запуском.

Как меняется состояние
  1. Сохранить R-7 и выполнить контрольное расшифрование
  2. Удалить DEK по его UUID
  3. Прочитать талон обычным менеджером
  4. Попробовать открыть поле новым клиентом, у которого нет сохранённого DEK

Короткий вызов

$receipt = $ce->deleteKey($id);
$number = $receipt->deletedCount;
$gone = $ce->getKey($id) === null;

Результат запуска

Что проверилиРезультат
До удаления талон читаетсяtrue
Удалено документов DEK1
Талон в бизнес-коллекции остался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 для чтения. На страницу выводится только результат сравнения.

Полная программа и проверенные значения

Устройство KMS, имена опций и идентификаторы

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, что и при обращении явно настроенного компонента.

Полная программа и проверенные значения

Устройство KMS, имена опций и идентификаторы

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. Его попытка расшифровать значение после удаления ключа завершается ошибкой.

Полная программа и проверенные значения

Устройство KMS, имена опций и идентификаторы

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, а список имён у него пуст.

Полная программа и проверенные значения

Устройство KMS, имена опций и идентификаторы

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);

Для неизвестной роли внутренней записи не потребовалось. Скрипт отличает такой исход от ошибки доступа.

Полная программа и проверенные значения

Устройство KMS, имена опций и идентификаторы

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 уже нет.

Полная программа и проверенные значения

Источники и обсуждения

Код и руководство с собой

Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.

Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.

Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.