Un guide des requêtes floues avec Apache ShardingSphere


  • Français


  • Apache ShardingSphere est une base de données distribuée open source et un écosystème dont les utilisateurs et les développeurs ont besoin pour leurs bases de données afin de fournir une expérience personnalisée et native du cloud. Sa dernière version contient de nombreuses nouvelles fonctionnalités, notamment le chiffrement des données intégré aux workflows SQL existants. Plus important encore, il permet des requêtes floues sur les données cryptées.

    Le problème

    En analysant l’entrée SQL d’un utilisateur et en réécrivant le SQL conformément aux règles de chiffrement de l’utilisateur, les données d’origine sont chiffrées et stockées simultanément avec les données de texte chiffré dans la base de données sous-jacente.

    Lorsqu’un utilisateur interroge les données, il extrait les données de texte chiffré de la base de données, les déchiffre et renvoie les données d’origine déchiffrées à l’utilisateur. Cependant, comme l’algorithme de chiffrement chiffre toute la chaîne, les utilisateurs ne peuvent pas exécuter de requêtes floues.

    Néanmoins, de nombreuses entreprises ont besoin de requêtes floues après le chiffrement des données. Dans la version 5.3.0, Apache ShardingSphere fournit aux utilisateurs un algorithme de requête floue par défaut qui prend en charge les champs chiffrés. L’algorithme prend également en charge le branchement à chaud, que les utilisateurs peuvent personnaliser. La requête floue peut être réalisée par configuration.

    Comment obtenir une requête floue dans des scénarios chiffrés

    Charger les données dans la base de données en mémoire (IMDB)

    Tout d’abord, chargez toutes les données dans l’IMDB pour les décrypter. Ensuite, ce sera comme interroger les données d’origine. Cette méthode peut réaliser des requêtes floues. Si la quantité de données est faible, cette méthode s’avérera simple et rentable. Cependant, si la quantité de données est importante, ce sera un désastre.

    Mettre en œuvre des fonctions de chiffrement et de déchiffrement compatibles avec les programmes de base de données

    La deuxième méthode consiste à modifier les conditions de requête floue et à utiliser la fonction de déchiffrement de la base de données pour déchiffrer d’abord les données, puis à implémenter la requête floue. L’avantage de cette méthode est le faible coût de mise en œuvre, de développement et d’utilisation.

    Les utilisateurs n’ont qu’à modifier légèrement les conditions de requête floues précédentes. Cependant, les fonctions de texte chiffré et de chiffrement sont stockées ensemble dans la base de données, ce qui ne peut pas faire face au problème des fuites de données de compte.

    SQL natif :

    select * from user where name like "%xxx%"

    Après avoir implémenté la fonction de déchiffrement :

    ѕеlесt * frоm uѕеr whеrе dесоdе(namе) lіkе "%ххх%"

    Stocker après masquage des données

    Implémentez le masquage des données sur le texte chiffré, puis stockez-le dans une colonne de requête floue. Cette méthode peut manquer de précision.

    Par exemple, numéro de portable 13012345678 devient 130****5678 après l’exécution de l’algorithme de masquage.

    Effectuer un stockage chiffré après la tokenisation et la combinaison

    Cette méthode effectue la tokenisation et la combinaison sur les données chiffrées, puis crypte le jeu de résultats en regroupant les caractères de longueur fixe et en divisant un champ en plusieurs. Par exemple, nous prenons quatre caractères anglais et deux caractères chinois comme condition de requête : ningyu1 utilise les quatre caractères en tant que groupe pour chiffrer, de sorte que le premier groupe est ningle deuxième groupe ingéréle troisième groupe ngyule quatrième groupe gyu1, etc. Tous les caractères sont chiffrés et stockés dans la colonne de requête floue. Si vous souhaitez récupérer toutes les données contenant quatre caractères, telles que ingérécryptez les caractères et utilisez une clé like"%partial%" interroger.

    Lacunes :

    1. Augmentation des coûts de stockage : le regroupement gratuit augmentera la quantité de données et la longueur des données augmentera après avoir été cryptées.
    2. Longueur limitée dans les requêtes floues : pour des raisons de sécurité, la longueur du regroupement libre ne peut pas être trop courte ou le tableau arc-en-ciel le cassera facilement. Comme dans l’exemple que j’ai mentionné ci-dessus, la longueur des caractères de requête floue doit être supérieure ou égale à quatre lettres/chiffres ou deux caractères chinois.

    Nos ressources préférées sur l’open source

    Algorithme de résumé à caractère unique (algorithme de requête floue par défaut fourni dans ShardingSphere version 5.3.0)

    Bien que les méthodes ci-dessus soient toutes viables, il est naturel de se demander s’il existe une meilleure alternative. Dans notre communauté, nous constatons que le chiffrement et le stockage à caractère unique peuvent équilibrer les performances et les requêtes, mais ne répondent pas aux exigences de sécurité.

    Alors quelle est la solution idéale ? Inspirés des algorithmes de masquage et des fonctions de hachage cryptographiques, nous constatons que la perte de données et les fonctions à sens unique peuvent être utilisées.

    La fonction de hachage cryptographique doit avoir les quatre fonctionnalités suivantes :

    1. Il devrait être facile de calculer la valeur de hachage pour un message donné.
    2. Il devrait être difficile de déduire le message d’origine à partir d’une valeur de hachage connue.
    3. Il ne devrait pas être possible de modifier le message sans changer la valeur de hachage.
    4. Il ne devrait y avoir qu’une très faible probabilité que deux messages différents produisent la même valeur de hachage.

    Sécurité : En raison de la fonction unidirectionnelle, il est impossible de déduire le message d’origine. Pour améliorer la précision de la requête floue, nous souhaitons chiffrer un seul caractère, mais la table arc-en-ciel le cassera.

    Nous prenons donc une fonction à sens unique (pour nous assurer que chaque caractère est le même après le cryptage) et augmentons la fréquence des collisions (pour nous assurer que chaque chaîne est 1 : N vers l’arrière), ce qui améliore considérablement la sécurité.

    Algorithme de requête floue

    Apache ShardingSphere implémente un algorithme de requête floue universel en utilisant l’algorithme de résumé à un seul caractère ci-dessous 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";
        }
    }
    • Définir le binaire mask code à perdre en précision 0b1111_0111_1101 (masque).
    • Enregistrez les caractères chinois courants avec un ordre perturbé comme un map dictionnaire.
    • Obtenir une seule chaîne de Unicode pour les chiffres, l’anglais et le latin.
    • Obtenir un index pour un caractère chinois appartenant à un dictionnaire.
    • D’autres personnages récupèrent le Unicode d’une seule chaîne.
    • Ajouter 1 (delta) aux chiffres obtenus par les différents types ci-dessus pour empêcher tout texte original d’apparaître dans la base de données.
    • Ensuite, convertissez le décalage Unicode en binaire, effectuez la AND fonctionnement avec masket effectuer une perte de deux chiffres.
    • Sortie directe des chiffres, de l’anglais et du latin après la perte de précision.
    • Les caractères restants sont convertis en décimal et sortis avec le caractère commun start code après la perte de précision.

    L’avancement du développement de l’algorithme flou

    La première édition

    Utilisez simplement Unicode et mask code de caractères communs pour effectuer la AND opération.

    Mask: 0b11111111111001111101
    The original character: 0b1000101110101111讯
    After encryption: 0b1000101000101101設

    En supposant que nous connaissions la clé et l’algorithme de chiffrement, la chaîne d’origine après un passage en arrière est :

    
    1.0b1000101100101101 謭
    2.0b1000101100101111 謯
    3.0b1000101110101101 训
    4.0b1000101110101111 讯
    5.0b1000101010101101 読
    6.0b1000101010101111 誯
    7.0b1000101000101111 訯
    8.0b1000101000101101 設

    Sur la base des bits manquants, nous constatons que chaque chaîne peut être dérivée 2^n Caractères chinois à l’envers. Quand le Unicode des caractères chinois communs est décimal, leurs intervalles sont très grands. Notez que les caractères chinois déduits à l’envers ne sont pas des caractères communs et qu’il est plus probable que les caractères d’origine soient déduits.

    (Xiong Gaoxiang, CC BY-SA 4.0)

    La deuxième édition

    L’intervalle des caractères chinois courants dans Unicode est irrégulier. Nous avions prévu de laisser les derniers morceaux de caractères chinois dans Unicode et les convertir en décimal comme un index pour récupérer certains caractères chinois courants. Ainsi, lorsque l’algorithme est connu, les caractères inhabituels n’apparaissent pas après une passe arrière et les distracteurs ne sont plus faciles à éliminer.

    Si nous laissons les derniers morceaux de caractères chinois dans Unicode, cela a quelque chose à voir avec la relation entre la précision de la requête floue et la complexité de l’anti-déchiffrement. Plus la précision est élevée, plus la difficulté de décryptage est faible.

    Examinons le degré de collision des caractères chinois courants sous notre algorithme :

    1. Quand mask=0b0011_1111_1111 :

    (Xiong Gaoxiang, CC BY-SA 4.0)

    2. Quand mask=0b0001_1111_1111 :

    (Xiong Gaoxiang, CC BY-SA 4.0)

    Pour la mantisse des caractères chinois, laissez 10 et 9 chiffres. La requête à 10 chiffres est plus précise car sa collision est beaucoup plus faible. Néanmoins, si l’algorithme et la clé sont connus, le texte original du caractère 1:1 peut être dérivé vers l’arrière.

    La requête à neuf chiffres est moins précise car les collisions à neuf chiffres sont relativement plus fortes, mais il y a moins de caractères 1:1. Bien que nous modifions les collisions, que nous laissions dix ou neuf chiffres, la distribution est déséquilibrée en raison de l’irrégularité Unicode de caractères chinois. La probabilité globale de collision ne peut pas être contrôlée.

    La troisième édition

    En réponse au problème de distribution inégale trouvé dans la deuxième édition, nous prenons des caractères communs avec un ordre perturbé comme table du dictionnaire.

    1. Le texte crypté recherche d’abord le index dans la table du dictionnaire dans le désordre. Nous utilisons le index et indice pour remplacer le Unicode sans règles. Utiliser Unicode en cas de caractères peu communs. (Remarque : Répartissez uniformément le code à calculer dans la mesure du possible.)

    2. L’étape suivante consiste à effectuer la AND fonctionnement avec un mask et perdre une précision de deux bits pour augmenter la fréquence des collisions.

    Examinons le degré de collision des caractères chinois courants sous notre algorithme :

    1. Quand mask=0b1111_1011_1101 :

    (Xiong Gaoxiang, CC BY-SA 4.0)

    2. Quand mask=0b0111_1011_1101 :

    (Xiong Gaoxiang, CC BY-SA 4.0)

    Quand le mask laisse 11 bits, vous pouvez voir que la distribution des collisions est concentrée à 1:4. Quand le mask laisse dix bits, le nombre devient 1:8. À ce stade, nous n’avons qu’à ajuster le nombre de pertes de précision pour contrôler si la collision est de 1:2, 1:4 ou 1:8.

    Si la mask est sélectionné comme 1, et que l’algorithme et la clé sont connus, il y aura un caractère chinois 1: 1 car nous calculons le degré de collision des caractères communs à ce moment. Si nous ajoutons les quatre bits manquants avant le binaire 16 bits des caractères chinois, la situation devient 2^5=32 cas.

    Étant donné que nous chiffrons l’intégralité du texte, même si le caractère individuel est déduit à l’envers, il y aura peu d’impact sur la sécurité globale et ne provoquera pas de fuites massives de données. Dans le même temps, le principe de la passe arrière est de connaître l’algorithme, la clé, deltaet dictionnaire, il est donc impossible d’y parvenir à partir des données de la base de données.

    Comment utiliser la requête floue

    La requête floue nécessite la configuration de encryptors (configuration de l’algorithme de chiffrement), likeQueryColumn (nom de colonne de requête floue) et likeQueryEncryptorName (nom de l’algorithme de chiffrement de la colonne de requête floue) dans la configuration de chiffrement.

    Veuillez vous référer à la configuration suivante. Ajoutez votre propre algorithme de partage et source de données.

    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

    Insérer

    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', '男')

    Mettre à jour

    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%'

    Sélectionner

    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 '佹%'

    Sélectionner : sous-requête de table fédérée

    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 '佹%')

    Supprimer

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

    L’exemple ci-dessus montre comment les colonnes de requête floue réécrivent SQL dans différentes syntaxes SQL pour prendre en charge les requêtes floues.

    Emballer

    Cet article vous a présenté les principes de fonctionnement de la requête floue et a utilisé des exemples spécifiques pour montrer comment l’utiliser. J’espère qu’à travers cet article, vous aurez une compréhension de base des requêtes floues.


    Cet article a été initialement publié le Moyen et a été republié avec la permission de l’auteur.

    Source

    Houssen Moshinaly

    Pour contacter personnellement le taulier :

    Laisser un commentaire

    Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

    Copy code