1. История

Apache ShardingSphere поддерживает шифрование данных. При анализе пользовательского ввода SQL и перезаписи SQL в соответствии с правилами шифрования пользователей исходные данные шифруются и одновременно сохраняются с данными зашифрованного текста в базовой базе данных.

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

Тем не менее многим предприятиям по-прежнему нужны нечеткие запросы после шифрования данных. В версии 5.3.0 Apache ShardingSphere предоставляет пользователям алгоритм нечетких запросов по умолчанию, поддерживающий нечеткие запросы для зашифрованных полей. Алгоритм также поддерживает горячее подключение, которое может быть настроено пользователями, а нечеткий запрос может быть выполнен с помощью конфигурации.

2. Как добиться нечеткого запроса в зашифрованных сценариях?

2.1 Загрузка данных в оперативную базу данных (IMDB)

Загрузите все данные в IMDB для их расшифровки; то это будет похоже на запрос исходных данных. Этот метод может выполнять нечеткие запросы. Если объем данных небольшой, этот метод окажется простым и экономичным, а с другой стороны, если объем данных большой, это обернется катастрофой.

2.2 Реализовать функции шифрования и дешифрования в соответствии с программами баз данных

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

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

Native SQL: select * from user where name like "%xxx%" 
After implementing the decryption function: ѕеlесt * frоm uѕеr whеrе dесоdе(namе) lіkе "%ххх%"

2.3 Сохранение после маскирования данных

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

For example, mobile number 13012345678 becomes 130****5678 after the masking algorithm is performed.

2.4 Выполнение зашифрованного хранения после токенизации и объединения

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

ningyu1 использует 4-символьную группу для шифрования, поэтому первая группа — ning, вторая — ingy, третья — ngyu, четвертая — gyu1 и так далее. Все символы зашифрованы и хранятся в столбце нечеткого запроса. Если вы хотите получить все данные, содержащие четыре символа, например ingy, зашифруйте символы и используйте для запроса ключ like"%partial%".

Недостатки:

  1. Увеличение затрат на хранение: бесплатная группировка увеличит объем данных, а длина данных после шифрования увеличится.
  2. Ограниченная длина в нечетком запросе: из соображений безопасности длина свободной группировки не может быть слишком короткой, иначе она будет легко взломана радужной таблицей. Как и в приведенном выше примере, длина символов нечеткого запроса должна быть больше или равна 4 буквам/цифрам или 2 китайским иероглифам.

2.5 Алгоритм односимвольного дайджеста (алгоритм нечетких запросов по умолчанию предоставляется в ShardingSphere версия 5.3.0)

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

Тогда каково идеальное решение? Вдохновленные алгоритмами маскирования и криптографическими хеш-функциями, мы обнаружили, что можно использовать потерю данных и односторонние функции.

Криптографическая хэш-функция должна иметь следующие четыре свойства:

  1. Для любого заданного сообщения должно быть легко вычислить хеш-значение.
  2. Должно быть трудно вывести исходное сообщение из известного значения хеш-функции.
  3. Не должно быть возможности изменить сообщение без изменения хеш-значения.
  4. Вероятность того, что два разных сообщения дадут одно и то же значение хеш-функции, должна быть очень низкой.

Безопасность: из-за односторонней функции невозможно определить исходное сообщение. Чтобы повысить точность нечеткого запроса, мы хотим зашифровать один символ, но он будет взломан радужной таблицей.

Поэтому мы берем одностороннюю функцию (чтобы убедиться, что все символы одинаковы после шифрования) и увеличиваем частоту коллизий (чтобы убедиться, что каждая строка является 1:N в обратном порядке), что значительно повышает безопасность.

3. Алгоритм нечетких запросов

Apache ShardingSphere реализует универсальный алгоритм нечетких запросов, используя приведенный ниже алгоритм односимвольного дайджеста org.apache.shardingsphere.encrypt.algorithm.like.CharDigestLikeEncryptAlgorithm.

public final class CharDigestLikeEncryptAlgorithm implements LikeEncryptAlgorithm<Object, String> {
  
    private static final String DELTA = "delta";
  
    private static final String MASK = "mask";
  
    private static final String START = "start";
  
    private static final String DICT = "dict";
  
    private static final int DEFAULT_DELTA = 1;
  
    private static final int DEFAULT_MASK = 0b1111_0111_1101;
  
    private static final int DEFAULT_START = 0x4e00;
  
    private static final int MAX_NUMERIC_LETTER_CHAR = 255;
  
    @Getter
    private Properties props;
  
    private int delta;
  
    private int mask;
  
    private int start;
  
    private Map<Character, Integer> charIndexes;
  
    @Override
    public void init(final Properties props) {
        this.props = props;
        delta = createDelta(props);
        mask = createMask(props);
        start = createStart(props);
        charIndexes = createCharIndexes(props);
    }
  
    private int createDelta(final Properties props) {
        if (props.containsKey(DELTA)) {
            String delta = props.getProperty(DELTA);
            try {
                return Integer.parseInt(delta);
            } catch (NumberFormatException ex) {
                throw new EncryptAlgorithmInitializationException("CHAR_DIGEST_LIKE", "delta can only be a decimal number");
            }
        }
        return DEFAULT_DELTA;
    }
  
    private int createMask(final Properties props) {
        if (props.containsKey(MASK)) {
            String mask = props.getProperty(MASK);
            try {
                return Integer.parseInt(mask);
            } catch (NumberFormatException ex) {
                throw new EncryptAlgorithmInitializationException("CHAR_DIGEST_LIKE", "mask can only be a decimal number");
            }
        }
        return DEFAULT_MASK;
    }
  
    private int createStart(final Properties props) {
        if (props.containsKey(START)) {
            String start = props.getProperty(START);
            try {
                return Integer.parseInt(start);
            } catch (NumberFormatException ex) {
                throw new EncryptAlgorithmInitializationException("CHAR_DIGEST_LIKE", "start can only be a decimal number");
            }
        }
        return DEFAULT_START;
    }
  
    private Map<Character, Integer> createCharIndexes(final Properties props) {
        String dictContent = props.containsKey(DICT) && !Strings.isNullOrEmpty(props.getProperty(DICT)) ? props.getProperty(DICT) : initDefaultDict();
        Map<Character, Integer> result = new HashMap<>(dictContent.length(), 1);
        for (int index = 0; index < dictContent.length(); index++) {
            result.put(dictContent.charAt(index), index);
        }
        return result;
    }
  
    @SneakyThrows
    private String initDefaultDict() {
        InputStream inputStream = CharDigestLikeEncryptAlgorithm.class.getClassLoader().getResourceAsStream("algorithm/like/common_chinese_character.dict");
        LineProcessor<String> lineProcessor = new LineProcessor<String>() {
  
            private final StringBuilder builder = new StringBuilder();
  
            @Override
            public boolean processLine(final String line) {
                if (line.startsWith("#") || 0 == line.length()) {
                    return true;
                } else {
                    builder.append(line);
                    return false;
                }
            }
  
            @Override
            public String getResult() {
                return builder.toString();
            }
        };
        return CharStreams.readLines(new InputStreamReader(inputStream, Charsets.UTF_8), lineProcessor);
    }
  
    @Override
    public String encrypt(final Object plainValue, final EncryptContext encryptContext) {
        return null == plainValue ? null : digest(String.valueOf(plainValue));
    }
  
    private String digest(final String plainValue) {
        StringBuilder result = new StringBuilder(plainValue.length());
        for (char each : plainValue.toCharArray()) {
            char maskedChar = getMaskedChar(each);
            if ('%' == maskedChar) {
                result.append(each);
            } else {
                result.append(maskedChar);
            }
        }
        return result.toString();
    }
  
    private char getMaskedChar(final char originalChar) {
        if ('%' == originalChar) {
            return originalChar;
        }
        if (originalChar <= MAX_NUMERIC_LETTER_CHAR) {
            return (char) ((originalChar + delta) & mask);
        }
        if (charIndexes.containsKey(originalChar)) {
            return (char) (((charIndexes.get(originalChar) + delta) & mask) + start);
        }
        return (char) (((originalChar + delta) & mask) + start);
    }
  
    @Override
    public String getType() {
        return "CHAR_DIGEST_LIKE";
    }
}
  • Определите двоичный код mask для потери точности 0b1111_0111_1101 (маска).
  • Сохраните распространенные китайские иероглифы с нарушенным порядком, как в словаре map.
  • Получите одну строку Unicode для цифр, английского и латинского языков.
  • Получите index для китайского символа, принадлежащего словарю.
  • Другие символы извлекают Unicode одной строки.
  • Добавьте 1 (delta) к цифрам, полученным разными типами выше, чтобы исходный текст не появился в базе данных.
  • Затем преобразуйте смещение Unicode в двоичное и выполните операцию AND с mask и выполните 2-битную потерю цифры.
  • Прямой вывод цифр, английского и латинского языков после потери точности.
  • Остальные символы преобразуются в десятичные и выводятся с общим символьным кодом start после потери точности.

4. Ход разработки нечеткого алгоритма

4.1 Первое издание

Просто используйте код общих символов Unicode и mask для выполнения операции AND.

Mask: 0b11111111111001111101
The original character: 0b1000101110101111讯
After encryption: 0b1000101000101101設
Assuming we know the key and encryption algorithm, the original string after a backward pass is:
1.0b1000101100101101 謭
2.0b1000101100101111 謯
3.0b1000101110101101 训
4.0b1000101110101111 讯
5.0b1000101010101101 読
6.0b1000101010101111 誯
7.0b1000101000101111 訯
8.0b1000101000101101 設

Мы обнаружили, что на основе недостающих битов каждая строка может быть получена на 2^n китайских символа назад. Когда Unicode обычных китайских иероглифов являются десятичными, их интервалы очень велики. Обратите внимание, что китайские иероглифы, выведенные в обратном порядке, не являются обычными иероглифами, и с большей вероятностью будут выведены исходные иероглифы.

4.2 Второе издание

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

Если мы оставим последние несколько битов китайских символов Unicode, это как-то связано с соотношением между точностью нечеткого запроса и сложностью анти-дешифрования. Чем выше точность, тем ниже сложность расшифровки.

Давайте посмотрим на степень столкновения распространенных китайских иероглифов в нашем алгоритме:

  1. Когда mask=0b0011_1111_1111:

2. Когда mask=0b0001_1111_1111:

Для мантиссы китайских иероглифов оставьте 10 и 9 цифр. 10-значный запрос более точен, потому что его коллизия намного слабее. Тем не менее, если алгоритм и ключ известны, исходный текст символа 1:1 можно вывести в обратном порядке.

9-значный запрос менее точен, потому что 9-значные коллизии относительно сильнее, но меньше символов 1:1. Мы обнаруживаем, что, хотя мы меняем коллизии независимо от того, оставляем ли мы 10 или 9 цифр, распределение очень несбалансированное из-за неправильного Unicode китайских символов, и общую вероятность коллизий нельзя контролировать.

4.3 Третье издание

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

  1. Зашифрованный текст сначала ищет index в таблице словаря вне порядка. Мы используем index и нижний индекс, чтобы заменить Unicode без правил.

Используйте Unicode в случае необычных символов. (Примечание: равномерно распределите вычисляемый код, насколько это возможно.)

2. Следующий шаг — выполнить операцию AND с mask и потерять 2-битную точность, чтобы увеличить частоту коллизий.

Давайте посмотрим на степень столкновения распространенных китайских иероглифов в нашем алгоритме:

  1. Когда mask=0b1111_1011_1101:

2. Когда mask=0b0111_1011_1101:

Когда mask оставляет 11 бит, вы можете видеть, что распределение коллизий сосредоточено на 1:4. Когда mask оставляет 10 бит, число становится 1:8. На данный момент нам нужно только настроить количество потерь точности, чтобы контролировать, будет ли столкновение 1: 2, 1: 4 или 1: 8.

Если mask выбрано как 1, а алгоритм и ключ известны, будет китайский символ 1:1, потому что то, что мы вычисляем в это время, является степенью столкновения общих символов. Если мы добавим недостающие 4 бита перед 16-битным двоичным кодом китайских иероглифов, ситуация станет 2^5=32 случаев.

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

5. Как использовать нечеткий запрос

Нечеткий запрос требует настройки encryptors (конфигурация алгоритма шифрования), likeQueryColumn (имя столбца нечеткого запроса) и likeQueryEncryptorName (имя алгоритма шифрования столбца нечеткого запроса) в конфигурации шифрования.

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

dataSources:
  ds_0:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.jdbc.Driver
    jdbcUrl: jdbc:mysql://127.0.0.1:3306/test?allowPublicKeyRetrieval=true
    username: root
    password: root
    
rules:
- !ENCRYPT
  encryptors:
    like_encryptor:
      type: CHAR_DIGEST_LIKE
    aes_encryptor:
      type: AES
      props:
        aes-key-value: 123456abc
  tables:
    user:
      columns:
        name:
          cipherColumn: name
          encryptorName: aes_encryptor
          assistedQueryColumn: name_ext
          assistedQueryEncryptorName: aes_encryptor
          likeQueryColumn: name_like
          likeQueryEncryptorName: like_encryptor
        phone:
          cipherColumn: phone
          encryptorName: aes_encryptor
          likeQueryColumn: phone_like
          likeQueryEncryptorName: like_encryptor
  queryWithCipherColumn: true


props:
  sql-show: true

Вставлять

Logic SQL: insert into user ( id, name, phone, sex) values ( 1, '熊高祥', '13012345678', '男')
Actual SQL: ds_0 ::: insert into user ( id, name, name_ext, name_like, phone, phone_like, sex) values (1, 'gyVPLyhIzDIZaWDwTl3n4g==', 'gyVPLyhIzDIZaWDwTl3n4g==', '佹堝偀', 'qEmE7xRzW0d7EotlOAt6ww==', '04101454589', '男')

Обновлять

Logic SQL: update user set name = '熊高祥123', sex = '男1' where sex ='男' and phone like '130%'
Actual SQL: ds_0 ::: update user set name = 'K22HjufsPPy4rrf4PD046A==', name_ext = 'K22HjufsPPy4rrf4PD046A==', name_like = '佹堝偀014', sex = '男1' where sex ='男' and phone_like like '041%'

Выбирать

Logic SQL: select * from user where (id = 1 or phone = '13012345678') and name like '熊%'
Actual SQL: ds_0 ::: select `user`.`id`, `user`.`name` AS `name`, `user`.`sex`, `user`.`phone` AS `phone`, `user`.`create_time` from user where (id = 1 or phone = 'qEmE7xRzW0d7EotlOAt6ww==') and name_like like '佹%'

Выберите: подзапрос объединенной таблицы

Logic SQL: select * from user LEFT JOIN user_ext on user.id=user_ext.id where user.id in (select id from user where sex = '男' and name like '熊%')
Actual SQL: ds_0 ::: select `user`.`id`, `user`.`name` AS `name`, `user`.`sex`, `user`.`phone` AS `phone`, `user`.`create_time`, `user_ext`.`id`, `user_ext`.`address` from user LEFT JOIN user_ext on user.id=user_ext.id where user.id in (select id from user where sex = '男' and name_like like '佹%')

Удалить

Logic SQL: delete from user where sex = '男' and name like '熊%'
Actual SQL: ds_0 ::: delete from user where sex = '男' and name_like like '佹%'

В приведенном выше примере показано, как столбцы нечетких запросов переписывают SQL с использованием различных синтаксисов SQL для поддержки нечетких запросов.

В этой записи блога вы познакомились с принципами работы нечетких запросов и использовали конкретные примеры, чтобы продемонстрировать, как их использовать. Мы надеемся, что благодаря этой статье вы получили базовое представление о нечетких запросах.

Ссылки

🔗 Ссылка для скачивания

🔗 Адрес проекта

🔗 ShardingSphere-on-Cloud

Автор

Сюн Гаосян, инженер Iflytek и участник ShardingSphere, отвечает за исследования и разработки в области шифрования и маскирования данных.