Почему нельзя фильтровать по CryptoField и как это обойти

09.04.2026

Проблема/контекст

CryptoField шифрует данные при записи и расшифровывает при чтении. Однако каждый вызов encrypt() в Bitrix\Main\Security\Cipher генерирует случайный вектор инициализации (IV). Одно и то же значение при каждом шифровании превращается в разный шифротекст. Это значит, что фильтрация через getList() по зашифрованному полю не вернет результатов: SQL-запрос сравнит открытый текст из фильтра с шифротекстом в базе и ничего не найдет.

На практике проблема возникает, как только нужно найти запись по API-токену, ключу доступа или другому зашифрованному идентификатору — задача, которая появляется в любой интеграции.

Решение с кодом

Добавьте к таблице индексируемую колонку с хешем открытого значения. Фильтруйте по хешу, а зашифрованное поле используйте только для чтения.

        <?php
declare(strict_types=1);

namespace Vendor\Integration\Model;

use Bitrix\Main\ORM\Data\DataManager;
use Bitrix\Main\ORM\Fields;
use Bitrix\Main\Security\Random;

final class ApiTokenTable extends DataManager
{
    public static function getTableName(): string
    {
        return 'vendor_api_tokens';
    }

    public static function getMap(): array
    {
        return [
            new Fields\IntegerField('ID', [
                'primary' => true,
                'autocomplete' => true,
            ]),
            new Fields\IntegerField('USER_ID', [
                'required' => true,
            ]),
            // Зашифрованный токен — только для хранения и чтения
            new Fields\CryptoField('TOKEN', [
                'crypto_enabled' => static::cryptoEnabled('TOKEN'),
            ]),
            // SHA-256 хеш токена — для поиска по индексу
            new Fields\StringField('TOKEN_HASH', [
                'size' => 64,
            ]),
        ];
    }

    // Создает запись с новым токеном
    public static function issueToken(int $userId): array
    {
        $token = Random::getString(32);

        $result = static::add([
            'USER_ID' => $userId,
            'TOKEN' => $token,
            'TOKEN_HASH' => hash('sha256', $token),
        ]);

        return ['id' => $result->getId(), 'token' => $token];
    }

    // Находит запись по открытому токену через хеш
    public static function findByToken(string $token): ?array
    {
        return static::getRow([
            'filter' => ['=TOKEN_HASH' => hash('sha256', $token)],
        ]);
    }
}

    

Схема: при создании записи токен шифруется CryptoField и сохраняется в TOKEN, а его SHA-256 хеш записывается в TOKEN_HASH. При входящем запросе вычисляется хеш и выполняется поиск по индексированной колонке. Исходный токен восстанавливается из зашифрованного поля автоматически при чтении.

Размер колонки TOKEN должен учитывать увеличение данных при шифровании. Для 32-байтного токена в режиме CTR минимальный размер: (32 + 16 + 32) * 1.5 ≈ 120 байт. Используйте VARCHAR(255). Колонка TOKEN_HASH фиксированная — CHAR(64) с уникальным индексом.

Еще одна деталь, которую стоит учитывать: если шифрование завершится ошибкой — например, ключ не настроен в .settings.php — метод encrypt() возвращает null, а не исходное значение. Данные молча теряются. Поэтому всегда проверяйте доступность шифрования через CryptoField::cryptoAvailable() перед включением криптополей, как это делается в ядре — например, в UserPhoneAuthTable для OTP-секретов.

Паттерн похож на хранение паролей, но с важным отличием: здесь исходное значение нужно восстановить, поэтому вместо необратимого bcrypt используется обратимое шифрование AES-256-CTR через CryptoField, а хеш служит только для поиска.

Итог

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

Опубликовано 1 месяц назад

Комментарии (0)

Пожалуйста, войдите в аккаунт, чтобы оставить комментарий

Оставить комментарий

Пока нет ни одного комментария. Будьте первым!

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