Um guia para consultas difusas com Apache ShardingSphere
Aprenda os princípios de funcionamento das consultas difusas e acompanhe exemplos específicos de como usá-las.
Apache ShardingSphere é um banco de dados distribuído de código aberto e um ecossistema que usuários e desenvolvedores precisam para que seus bancos de dados forneçam uma experiência personalizada e nativa da nuvem. Sua versão mais recente contém muitos recursos novos, incluindo criptografia de dados integrada aos fluxos de trabalho SQL existentes. Mais importante ainda, permite consultas difusas dos dados criptografados.
O problema
Ao analisar a entrada SQL de um usuário e reescrever o SQL de acordo com as regras de criptografia do usuário, os dados originais são criptografados e armazenados simultaneamente com dados de texto cifrado no banco de dados subjacente.
Quando um usuário consulta os dados, ele busca os dados do texto cifrado do banco de dados, descriptografa-os e retorna os dados originais descriptografados ao usuário. No entanto, como o algoritmo de criptografia criptografa toda a sequência, os usuários não podem executar consultas difusas.
No entanto, muitas empresas precisam de consultas difusas depois que os dados são criptografados. Na versão 5.3.0, o Apache ShardingSphere fornece aos usuários um algoritmo de consulta difusa padrão que suporta campos criptografados. O algoritmo também suporta hot plugging, que os usuários podem personalizar. A consulta difusa pode ser obtida por meio de configuração.
Como obter consultas difusas em cenários criptografados
Carregar dados no banco de dados na memória (IMDB)
Primeiro, carregue todos os dados no IMDB para descriptografá-los. Então, será como consultar os dados originais. Este método pode obter consultas difusas. Se a quantidade de dados for pequena, este método será simples e econômico. Porém, se a quantidade de dados for grande, será um desastre.
Implementar funções de criptografia e descriptografia consistentes com programas de banco de dados
O segundo método é modificar as condições de consulta difusa e usar a função de descriptografia do banco de dados para descriptografar os dados primeiro e depois implementar a consulta difusa. A vantagem deste método é o baixo custo de implementação, desenvolvimento e uso.
Os usuários só precisam modificar ligeiramente as condições de consulta difusa anteriores. No entanto, o texto cifrado e as funções de criptografia são armazenados juntos no banco de dados, o que não consegue lidar com o problema de vazamento de dados de contas.
SQL nativo:
select * from user where name like "%xxx%"
Depois de implementar a função de descriptografia:
ѕеlесt * frоm uѕеr whеrе dесоdе(namе) lіkе "%ххх%"
Armazenar após mascaramento de dados
Implemente o mascaramento de dados no texto cifrado e armazene-o em uma coluna de consulta difusa. Este método pode carecer de precisão.
Por exemplo, o número de celular 13012345678 se torna 130****5678 após a execução do algoritmo de mascaramento.
Execute o armazenamento criptografado após tokenização e combinação
Este método executa tokenização e combinação em dados de texto cifrado e, em seguida, criptografa o conjunto de resultados agrupando caracteres com comprimento fixo e dividindo um campo em vários. Por exemplo, pegamos quatro caracteres ingleses e dois caracteres chineses como condição de consulta: ningyu1 usa os quatro caracteres como um grupo para criptografar, então o primeiro grupo é ning, o segundo grupo ingy, o terceiro grupo ngyu, o quarto grupo gyu1 e assim por diante. Todos os caracteres são criptografados e armazenados na coluna de consulta difusa. Se você deseja recuperar todos os dados que contêm quatro caracteres, como ingy, criptografe os caracteres e use uma chave como "%partial%"
para consultar.
Deficiências:
- Aumento dos custos de armazenamento: o agrupamento gratuito aumentará a quantidade de dados e o comprimento dos dados aumentará após serem criptografados.
- Comprimento limitado na consulta difusa: devido a questões de segurança, o comprimento do agrupamento livre não pode ser muito curto ou a tabela arco-íris irá quebrá-lo facilmente. Como no exemplo que mencionei acima, o comprimento dos caracteres de consulta difusa deve ser maior ou igual a quatro letras/dígitos ou dois caracteres chineses.
Algoritmo de resumo de caractere único (algoritmo de consulta difusa padrão fornecido no ShardingSphere versão 5.3.0)
Embora todos os métodos acima sejam viáveis, é natural perguntar se existe uma alternativa melhor. Em nossa comunidade, descobrimos que a criptografia e o armazenamento de um único caractere podem equilibrar desempenho e consulta, mas não atendem aos requisitos de segurança.
Então, qual é a solução ideal? Inspirados em algoritmos de mascaramento e funções hash criptográficas, descobrimos que perda de dados e funções unidirecionais podem ser usadas.
A função hash criptográfica deve ter os quatro recursos a seguir:
- Deve ser fácil calcular o valor hash para qualquer mensagem.
- Deve ser difícil inferir a mensagem original a partir de um valor hash conhecido.
- Não deveria ser viável modificar a mensagem sem alterar o valor do hash.
- Deve haver apenas uma chance muito baixa de que duas mensagens diferentes produzam o mesmo valor de hash.
Segurança: Devido à função unidirecional, é impossível inferir a mensagem original. Para melhorar a precisão da consulta difusa, queremos criptografar um único caractere, mas a tabela arco-íris irá quebrá-lo.
Portanto, adotamos uma função unidirecional (para garantir que todos os caracteres sejam iguais após a criptografia) e aumentamos a frequência de colisões (para garantir que cada string seja 1: N invertida), o que aumenta muito a segurança.
Algoritmo de consulta difusa
Apache ShardingSphere implementa um algoritmo de consulta difusa universal usando o algoritmo de resumo de caractere único abaixo 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";
}
}
- Defina o código binário
mask
para perder precisão0b1111_0111_1101
(mask). - Salve caracteres chineses comuns com ordem interrompida, como um dicionário
mapa
. - Obtenha uma única string de
Unicode
para dígitos, inglês e latim. - Obtenha um
índice
para um caractere chinês pertencente a um dicionário. - Outros caracteres buscam o
Unicode
de uma única string. - Adicione
1 (delta)
aos dígitos obtidos pelos diferentes tipos acima para evitar que qualquer texto original apareça no banco de dados. - Em seguida, converta o deslocamento
Unicode
em binário, execute a operaçãoAND
commask
e execute uma perda de dígitos de dois bits. - Produza diretamente dígitos, inglês e latim após a perda de precisão.
- Os caracteres restantes são convertidos em decimais e gerados com o código de caractere comum
start
após a perda de precisão.
O progresso do desenvolvimento do algoritmo difuso
A primeira edição
Basta usar o código Unicode
e mask
de caracteres comuns para realizar a operação AND
.
Mask: 0b11111111111001111101
The original character: 0b1000101110101111讯
After encryption: 0b1000101000101101設
Supondo que conhecemos a chave e o algoritmo de criptografia, a string original após uma passagem para trás é:
1.0b1000101100101101 謭
2.0b1000101100101111 謯
3.0b1000101110101101 训
4.0b1000101110101111 讯
5.0b1000101010101101 読
6.0b1000101010101111 誯
7.0b1000101000101111 訯
8.0b1000101000101101 設
Com base nos bits ausentes, descobrimos que cada string pode ser derivada de 2^n
caracteres chineses de trás para frente. Quando o Unicode
dos caracteres chineses comuns é decimal, seus intervalos são muito grandes. Observe que os caracteres chineses inferidos de trás para frente não são caracteres comuns e é mais provável que sejam inferidos os caracteres originais.
(Xiong Gaoxiang, CC BY-SA 4.0)
A segunda edição
O intervalo de caracteres chineses comuns em Unicode
é irregular. Planejamos deixar os últimos bits de caracteres chineses em Unicode
e convertê-los em decimais como um índice
para buscar alguns caracteres chineses comuns. Dessa forma, quando o algoritmo for conhecido, caracteres incomuns não aparecerão após uma passagem para trás e os distratores não serão mais fáceis de eliminar.
Se deixarmos os últimos bits dos caracteres chineses em Unicode
, isso terá algo a ver com a relação entre a precisão da consulta difusa e a complexidade anti-descriptografia. Quanto maior a precisão, menor a dificuldade de descriptografia.
Vamos dar uma olhada no grau de colisão de caracteres chineses comuns em nosso algoritmo:
1. Quando máscara
=0b0011_1111_1111:
(Xiong Gaoxiang, CC BY-SA 4.0)
2. Quando máscara
=0b0001_1111_1111:
(Xiong Gaoxiang, CC BY-SA 4.0)
Para a mantissa de caracteres chineses, deixe 10 e 9 dígitos. A consulta de 10 dígitos é mais precisa porque sua colisão é muito mais fraca. No entanto, se o algoritmo e a chave forem conhecidos, o texto original do caractere 1:1 pode ser derivado de trás para frente.
A consulta de nove dígitos é menos precisa porque as colisões de nove dígitos são relativamente mais fortes, mas há menos caracteres 1:1. Embora alteremos as colisões independentemente de deixarmos dez ou nove dígitos, a distribuição é desequilibrada devido ao Unicode
irregular dos caracteres chineses. A probabilidade geral de colisão não pode ser controlada.
A terceira edição
Em resposta ao problema de distribuição desigual encontrado na segunda edição, consideramos os caracteres comuns com ordem interrompida como a tabela do dicionário.
1. O texto criptografado primeiro consulta o índice
na tabela do dicionário fora de ordem. Usamos o índice
e o subscrito para substituir o Unicode
sem regras. Use Unicode
no caso de caracteres incomuns. (Observação: distribua uniformemente o código a ser calculado, tanto quanto possível.)
2. O próximo passo é realizar a operação AND
com uma máscara
e perder a precisão de dois bits para aumentar a frequência das colisões.
Vamos dar uma olhada no grau de colisão de caracteres chineses comuns em nosso algoritmo:
1. Quando máscara
=0b1111_1011_1101:
(Xiong Gaoxiang, CC BY-SA 4.0)
2. Quando máscara
=0b0111_1011_1101:
(Xiong Gaoxiang, CC BY-SA 4.0)
Quando a máscara deixa 11 bits, você pode ver que a distribuição de colisão está concentrada em 1:4. Quando a máscara
deixa dez bits, o número passa a ser 1:8. Neste momento, só precisamos ajustar o número de perdas de precisão para controlar se a colisão é 1:2, 1:4 ou 1:8.
Se a máscara
for selecionada como 1 e o algoritmo e a chave forem conhecidos, haverá um caractere chinês 1:1 porque calculamos o grau de colisão de caracteres comuns neste momento. Se adicionarmos os quatro bits que faltam antes do binário de 16 bits dos caracteres chineses, a situação se torna 2^5=32
casos.
Como criptografamos todo o texto, mesmo que o caractere individual seja inferido de trás para frente, haverá pouco impacto na segurança geral e não causará vazamentos de dados em massa. Ao mesmo tempo, a premissa da passagem para trás é conhecer o algoritmo, a chave, o delta
e o dicionário, portanto é impossível conseguir isso a partir dos dados do banco de dados.
Como usar consulta difusa
A consulta difusa requer a configuração de criptografadores
(configuração do algoritmo de criptografia), likeQueryColumn
(nome da coluna da consulta difusa) e likeQueryEncryptorName
(nome do algoritmo de criptografia do fuzzy coluna de consulta) na configuração de criptografia.
Consulte a seguinte configuração. Adicione seu próprio algoritmo de fragmentação e fonte de dados.
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
Inserir
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', '男')
Atualizar
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%'
Selecione
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 '佹%'
Selecione: subconsulta de tabela federada
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 '佹%')
Excluir
Logic SQL: delete from user where sex = '男' and name like '熊%'
Actual SQL: ds_0 ::: delete from user where sex = '男' and name_like like '佹%'
O exemplo acima demonstra como colunas de consulta difusa reescrevem SQL em diferentes sintaxes SQL para suportar consultas difusas.
Embrulhar
Este artigo apresentou os princípios de funcionamento da consulta difusa e usou exemplos específicos para demonstrar como usá-la. Espero que, através deste artigo, você tenha uma compreensão básica das consultas difusas.
Este artigo foi publicado originalmente no Medium e republicado com a permissão do autor.