Un guide des requêtes floues avec Apache ShardingSphere

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 :
- 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.
- 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.
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 :
- Il devrait être facile de calculer la valeur de hachage pour un message donné.
- Il devrait être difficile de déduire le message d’origine à partir d’une valeur de hachage connue.
- Il ne devrait pas être possible de modifier le message sans changer la valeur de hachage.
- 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écision0b1111_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 laAND
fonctionnement avecmask
et 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é, delta
et 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.