preface

This chapter is based on the official document of shardingSphere data encryption implementation principle, according to the migration before, migration, migration after three stages, observe the changes in SQL execution, in-depth source analysis of the principle.

It is recommended to read the following article first:

  • Data desensitization implementation principle
  • ShardingJDBC source code to read (six) rewrite

First, preparation

Fragment Policy Configuration

Database based on user_id%2 and table based on user_id%4/2. User_id =1 is assigned to ds_1.my_user_0; User_id =2 is allocated to ds_0.my_user_0; User_id =3 is assigned to ds_1.my_user_1.

public DataSource getDataSource(a) throws SQLException {
    ShardingRuleConfiguration shardingConfig = new ShardingRuleConfiguration();
    / / table rules
    TableRuleConfiguration userTable = new TableRuleConfiguration("my_user"."ds_${0.. 1}.my_user_${[0, 1]}");
    // Library split policy
    ShardingStrategyConfiguration shardingDb = new InlineShardingStrategyConfiguration("id"."ds_${id%2}");
    userTable.setDatabaseShardingStrategyConfig(shardingDb);
    // Table split policy
    ShardingStrategyConfiguration shardingTable = new StandardShardingStrategyConfiguration("id", (PreciseShardingAlgorithm<Integer>) (availableTargetNames, shardingValue) -> {
        Integer l = shardingValue.getValue() % 4 / 2;
        return availableTargetNames.stream().filter(t -> t.endsWith(String.valueOf(l))).findFirst().orElse(null);
    });
    userTable.setTableShardingStrategyConfig(shardingTable);
    shardingConfig.setTableRuleConfigs(Collections.singletonList(userTable));
    // Encryption policy
    EncryptRuleConfiguration encryptRuleConfig = createEncryptRuleConfig();
    shardingConfig.setEncryptRuleConfig(encryptRuleConfig);
    // Global configuration
    Properties properties = new Properties();
    properties.setProperty(ConfigurationPropertyKey.SQL_SHOW.getKey(), "true");
    // query.with.cipher.column
    properties.setProperty(ConfigurationPropertyKey.QUERY_WITH_CIPHER_COLUMN.getKey(), "false");
    return ShardingDataSourceFactory.createDataSource(createDataSourceMap(), shardingConfig, properties);
}
Copy the code

DDL

private static void addCipherColumn(Connection connection) throws SQLException {
  String sql = "alter table my_user add column pwd_encrypt varchar(255) not null";
  Statement statement = connection.createStatement();
  statement.execute(sql);
  statement.close();
}
Copy the code

The encryption configuration

private EncryptRuleConfiguration createEncryptRuleConfig(a) {
  EncryptRuleConfiguration configuration = new EncryptRuleConfiguration();
  // Encrypt and decrypt
  Properties properties = new Properties();
  properties.setProperty("aes.key.value"."123456");
  EncryptorRuleConfiguration aesRuleConfiguration = new EncryptorRuleConfiguration("aes", properties);
  configuration.getEncryptors().put("user_pwd_encryptor", aesRuleConfiguration);
  // table - field - encryption rules
  EncryptColumnRuleConfiguration pwdEncryptConfig = new EncryptColumnRuleConfiguration("pwd"."pwd_encrypt".""."user_pwd_encryptor");
  Map<String, EncryptColumnRuleConfiguration> columns = new HashMap<>();
  columns.put("pwd", pwdEncryptConfig);// Logical field - encryption configuration
  configuration.getTables().put("my_user".new EncryptTableRuleConfiguration(columns));
  return configuration;
}
Copy the code

Before migration: Query.with.cipher. column=false

Old user

The query

Logic SQL: select * from my_user where id = ? Actual SQL: ds_1 ::: select id, pwd AS pwd from my_user_1 where id = ? ::: [3] Logic SQL: select id, pwd from my_user where id = ? Actual SQL: ds_1 ::: select id, pwd AS pwd from my_user_1 where id = ? ::: [3] Logic SQL: select * from my_user where pwd = ? Actual SQL: ds_0 ::: select id, pwd AS pwd from my_user_0 where pwd = ? ::: [1608881254186] Actual SQL: ds_0 ::: select id, pwd AS pwd from my_user_1 where pwd = ? ::: [1608881254186] Actual SQL: ds_1 ::: select id, pwd AS pwd from my_user_0 where pwd = ? ::: [1608881254186] Actual SQL: ds_1 ::: select id, pwd AS pwd from my_user_1 where pwd = ? : : : [1608881254186]Copy the code

Conclusion:

  • * will be rewritten as id, PWD as PWD; Id, PWD will be rewritten As PWD As PWD.
  • Where query conditions will not be overridden.

Why was the query field overwritten?

Review SQLRewriteEntry to create AN SQL rewrite context.

When enabling the encryption rule configuration, SQL overwrites SQLRewriteEntry#createSQLRewriteContext to do two things, One is to perform EncryptSQLRewriteContextDecorator put encryption related Token generator (SQLTokenGenerator) in rewriting context, another is generated SQLToken, Whereas SQLToken, if implemented by Substitutable, would result in partial string substitution for SQL (see AbstractSQLBuilder#toSQL)

public SQLRewriteContext createSQLRewriteContext(final String sql, final List<Object> parameters, final SQLStatementContext sqlStatementContext, final RouteContext routeContext) {
  SQLRewriteContext result = new SQLRewriteContext(schemaMetaData, sqlStatementContext, sql, parameters);
  / / perform all SQLRewriteContextDecorator, rewrite the Param, create SQLTokenGenerators
  decorate(decorators, result, routeContext);
  / / generated SQLToken
  result.generateSQLTokens();
  return result;
}
Copy the code

EncryptSQLRewriteContextDecorator will put EncryptProjectionTokenGenerator in generator list, will eventually perform EncryptProjectionTokenGenerator.

private Collection<SubstitutableColumnNameToken> generateSQLTokens(final ProjectionsSegment segment, final String tableName, 
                                                                       final SelectStatementContext selectStatementContext, final EncryptTable encryptTable) {
  Collection<SubstitutableColumnNameToken> result = new LinkedList<>();
  for (ProjectionSegment each : segment.getProjections()) {
      if (each instanceof ColumnProjectionSegment) { // id, pwd
          if(encryptTable.getLogicColumns().contains(((ColumnProjectionSegment) each).getColumn().getIdentifier().getValue())) { result.add(generateSQLToken((ColumnProjectionSegment) each, tableName)); }}if (each instanceof ShorthandProjectionSegment) { / / *result.add(generateSQLToken((ShorthandProjectionSegment) each, selectStatementContext.getProjectionsContext(), tableName, encryptTable)); }}return result;
}
Copy the code

For ShorthandProjectionSegment, such as select * will cycle all actual field (from TableMetaData, pay attention to the cipher text fields will not exist in the list, EncryptTableMetaDataDecorator when adornment TableMetaData, ruled out the cipher text field), common fields will directly join the result set, encryption rule configuration logic field may be rewritten.

private SubstitutableColumnNameToken generateSQLToken(final ShorthandProjectionSegment segment, 
                                                          final ProjectionsContext projectionsContext, final String tableName, final EncryptTable encryptTable) {
    ShorthandProjection shorthandProjection = getShorthandProjection(segment, projectionsContext);
    List<String> shorthandExtensionProjections = new LinkedList<>();
    // Get all the actual columns
    for (ColumnProjection each : shorthandProjection.getActualColumns()) {
        // If the logical fields match the actual SQL fields, they may be overwritten
        if (encryptTable.getLogicColumns().contains(each.getName())) {
            shorthandExtensionProjections.add(new ColumnProjection(each.getOwner(), getEncryptColumnName(tableName, each.getName()), each.getName()).getExpressionWithAlias());
        } else {
        // Add other common fields directlyshorthandExtensionProjections.add(each.getExpression()); }}return new SubstitutableColumnNameToken(segment.getStartIndex(), segment.getStopIndex(), Joiner.on(",").join(shorthandExtensionProjections));
}
Copy the code

The getEncryptColumnName method returns the ciphertext field if query.with.cipher.column is true, otherwise it returns the plaintext field.

private String getEncryptColumnName(final String tableName, final String logicEncryptColumnName) {
    // Locate the original column according to the logicEncryptColumnName logical column name
    Optional<String> plainColumn = getEncryptRule().findPlainColumn(tableName, logicEncryptColumnName);
    // If the original field exists and query.with.cipher.column=false, the original field is returned
    // If the original field does not exist or query.with.cipher.column=true, the ciphertext field is returned
    returnplainColumn.isPresent() && ! queryWithCipherColumn ? plainColumn.get() : getEncryptRule().getCipherColumn(tableName, logicEncryptColumnName); }Copy the code

ColumnProjectionSegment, such as ID and PWD. The PWD matches the logical field of the encryption rule and enters the generateSQLToken method. The getEncryptColumnName method is also used to generate the original text or ciphertext. If there is no custom AS alias in SQL, the logical field name is spelled.

private SubstitutableColumnNameToken generateSQLToken(final ColumnProjectionSegment segment, final String tableName) {
    String encryptColumnName = getEncryptColumnName(tableName, segment.getColumn().getIdentifier().getValue());
    if(! segment.getAlias().isPresent()) {// Splicing the AS logical column names to ensure that client logic is not affected
        encryptColumnName += " AS " + segment.getColumn().getIdentifier().getValue();
    }
    return segment.getColumn().getOwner().isPresent() 
    	? new SubstitutableColumnNameToken(segment.getColumn().getOwner().get().getStopIndex() + 2, segment.getStopIndex(), encryptColumnName)
        : new SubstitutableColumnNameToken(segment.getStartIndex(), segment.getStopIndex(), encryptColumnName);
}
Copy the code

update

Logic SQL: update my_user set pwd = ? where id = ? Actual SQL: ds_1 ::: update my_user_1 set pwd_encrypt = ? , pwd = ? where id = ? ::: [UVZAHLRN/LfAgbNp2BP7KA==, 1608881254186, 3]Copy the code

Why does updating a field add an encryption field?

1. Added SQLToken

Locate SQLTokenGenerators#generateSQLTokens to see which SQLTokenGenerator creates a new SQLToken. Locate the EncryptAssignmentTokenGenerator soon.

public final class EncryptAssignmentTokenGenerator extends BaseEncryptSQLTokenGenerator implements CollectionSQLTokenGenerator {
    
  @Override
  public Collection<EncryptAssignmentToken> generateSQLTokens(final SQLStatementContext sqlStatementContext) {
      Collection<EncryptAssignmentToken> result = new LinkedList<>();
      String tableName = ((TableAvailable) sqlStatementContext).getAllTables().iterator().next().getTableName().getIdentifier().getValue();
      // Loop through param for each update, such as PWD
      for (AssignmentSegment each : getSetAssignmentSegment(sqlStatementContext.getSqlStatement()).getAssignments()) {
          / / find EncryptColumnRuleConfiguration according to the logical name
          / / based on the encryptor find EncryptorRuleConfiguration EncryptColumnRuleConfiguration
          if (getEncryptRule().findEncryptor(tableName, each.getColumn().getIdentifier().getValue()).isPresent()) {
              // Create SQLToken and put resultgenerateSQLToken(tableName, each).ifPresent(result::add); }}returnresult; }}Copy the code

EncryptAssignmentToken update my_user set PWD =? Param pattern with placeholder; Update my_user set PWD = ‘XXX’ Literal mode

private Optional<EncryptAssignmentToken> generateSQLToken(final String tableName, final AssignmentSegment assignmentSegment) {
  // param
  if (assignmentSegment.getValue() instanceof ParameterMarkerExpressionSegment) {
      return Optional.of(generateParameterSQLToken(tableName, assignmentSegment));
  }
  // literal
  if (assignmentSegment.getValue() instanceof LiteralExpressionSegment) {
      return Optional.of(generateLiteralSQLToken(tableName, assignmentSegment));
  }
  return Optional.empty();
}
Copy the code

Take a look at the Param schema.

private EncryptAssignmentToken generateParameterSQLToken(final String tableName, final AssignmentSegment assignmentSegment) {
  EncryptParameterAssignmentToken result = new EncryptParameterAssignmentToken(assignmentSegment.getColumn().getStartIndex(), assignmentSegment.getStopIndex());
  String columnName = assignmentSegment.getColumn().getIdentifier().getValue();
  // Add the ciphertext column
  addCipherColumn(tableName, columnName, result);
  // Add the secondary query column
  addAssistedQueryColumn(tableName, columnName, result);
  // Add the original column
  addPlainColumn(tableName, columnName, result);
  return result;
}
Copy the code

EncryptParameterAssignmentToken is eventually generated SQLToken, focus on the toString method, as after AbstractSQLBuilder# toSQL splicing SQL String.

public final class EncryptParameterAssignmentToken extends EncryptAssignmentToken {
    private final Collection<String> columnNames = new LinkedList<>();
    public EncryptParameterAssignmentToken(final int startIndex, final int stopIndex) {
        super(startIndex, stopIndex);
    }
    public void addColumnName(final String columnName) {
        columnNames.add(columnName);
    }
    // Return pwd_encrypt =? , pwd = ?
    @Override
    public String toString(a) {
        Collection<String> items = Collections2.transform(columnNames, input -> String.format("%s = ?", input));
        return Joiner.on(",").join(items); }}Copy the code

2. Added Param

SQLRewriteEntry# createSQLRewriteContext, performed SQLRewriteContextDecorator rewrite param first, and then execute SQLTokenGenerators create SQLToken.

public SQLRewriteContext createSQLRewriteContext(final String sql, final List<Object> parameters, final SQLStatementContext sqlStatementContext, final RouteContext routeContext) {
  SQLRewriteContext result = new SQLRewriteContext(schemaMetaData, sqlStatementContext, sql, parameters);
  / / perform all SQLRewriteContextDecorator, rewrite the Param, create SQLTokenGenerators
  decorate(decorators, result, routeContext);
  / / generated SQLToken
  result.generateSQLTokens();
  return result;
}
Copy the code

Performs all ParameterRewriter EncryptSQLRewriteContextDecorator.

public final class EncryptSQLRewriteContextDecorator implements SQLRewriteContextDecorator<EncryptRule> {
    
@Override
public void decorate(final EncryptRule encryptRule, final ConfigurationProperties properties, final SQLRewriteContext sqlRewriteContext) {
    boolean isQueryWithCipherColumn = properties.<Boolean>getValue(ConfigurationPropertyKey.QUERY_WITH_CIPHER_COLUMN);
    // Construct the ParameterRewriter list
    for (ParameterRewriter each : new EncryptParameterRewriterBuilder(encryptRule, isQueryWithCipherColumn).getParameterRewriters(sqlRewriteContext.getSchemaMetaData())) {
        if(! sqlRewriteContext.getParameters().isEmpty() && each.isNeedRewrite(sqlRewriteContext.getSqlStatementContext())) {/ / ParameterRewriter execution
            each.rewrite(sqlRewriteContext.getParameterBuilder(), sqlRewriteContext.getSqlStatementContext(), sqlRewriteContext.getParameters());
        }
    }
    sqlRewriteContext.addSQLTokenGenerators(new EncryptTokenGenerateBuilder(encryptRule, isQueryWithCipherColumn).getSQLTokenGenerators());
}
Copy the code

The final location to EncryptAssignmentParameterRewriter.

public final class EncryptAssignmentParameterRewriter extends EncryptParameterRewriter<SQLStatementContext> {
    
  @Override
  public void rewrite(final ParameterBuilder parameterBuilder, final SQLStatementContext sqlStatementContext, final List<Object> parameters) {
      String tableName = ((TableAvailable) sqlStatementContext).getAllTables().iterator().next().getTableName().getIdentifier().getValue();
      for (AssignmentSegment each : getSetAssignmentSegment(sqlStatementContext.getSqlStatement()).getAssignments()) {
          // Is the parameter mode, and this is a logical field in the encryption rule configuration
          if (each.getValue() instanceof ParameterMarkerExpressionSegment && getEncryptRule().findEncryptor(tableName, each.getColumn().getIdentifier().getValue()).isPresent()) {
              StandardParameterBuilder standardParameterBuilder = parameterBuilder instanceof StandardParameterBuilder
                      ? (StandardParameterBuilder) parameterBuilder : ((GroupedParameterBuilder) parameterBuilder).getParameterBuilders().get(0);
              // Execute the parameters overrideencryptParameters(standardParameterBuilder, tableName, each, parameters); }}}}Copy the code

The encryptParameters method overrides the parameter list in StandardParameterBuilder.

private void encryptParameters(final StandardParameterBuilder parameterBuilder, final String tableName, final AssignmentSegment assignmentSegment, final List<Object> parameters) {
  String columnName = assignmentSegment.getColumn().getIdentifier().getValue();
  // The initial PWD value (plaintext) corresponds to the subscript
  int parameterMarkerIndex = ((ParameterMarkerExpressionSegment) assignmentSegment.getValue()).getParameterMarkerIndex();
  // PWD initial value (plain text)
  Object originalValue = parameters.get(parameterMarkerIndex);
  // Find the Encryptor and execute the encryption to get the encrypted value
  Object cipherValue = getEncryptRule().getEncryptValues(tableName, columnName, Collections.singletonList(originalValue)).iterator().next();
  // Replace the plaintext value with the encrypted value
  parameterBuilder.addReplacedParameters(parameterMarkerIndex, cipherValue);
  Collection<Object> addedParameters = new LinkedList<>();
  // Add the auxiliary query value
  if (getEncryptRule().findAssistedQueryColumn(tableName, columnName).isPresent()) {
      Object assistedQueryValue = getEncryptRule().getEncryptAssistedQueryValues(tableName, columnName, Collections.singletonList(originalValue)).iterator().next();
      addedParameters.add(assistedQueryValue);
  }
  // Add the original value
  if (getEncryptRule().findPlainColumn(tableName, columnName).isPresent()) {
      addedParameters.add(originalValue);
  }
  if(! addedParameters.isEmpty()) { parameterBuilder.addAddedParameters(parameterMarkerIndex +1, addedParameters); }}Copy the code

Replace the original value with the ciphertext value, then check whether the original field exists in the encryption rule, and then add the original value again. Instead of adding the ciphertext value directly? This involves deleting the original column after the fourth migration, because the original column cannot exist forever.

A new user

new

Logic SQL: insert into my_user (id, pwd) values (?, ?) 
Actual SQL: ds_0 ::: insert into my_user_1 (id, pwd_encrypt, pwd) values (?, ?, ?) ::: [2, TcZjXe0lAwrT19nIJ9+0Iw==, 1608881254477]
Copy the code

Why does an insert statement add an encryption field?

1. Added Param

EncryptInsertValueParameterRewriter perform encryption param rewrite rules the insert statement.

@Override
public void rewrite(final ParameterBuilder parameterBuilder, final InsertStatementContext insertStatementContext, final List<Object> parameters) {
    String tableName = insertStatementContext.getSqlStatement().getTable().getTableName().getIdentifier().getValue();
    // Field names in reverse order
    Iterator<String> descendingColumnNames = insertStatementContext.getDescendingColumnNames();
    while (descendingColumnNames.hasNext()) {
        String columnName = descendingColumnNames.next();
        getEncryptRule()
                Find the Encryptor / /
                .findEncryptor(tableName, columnName)
                // Rewrite Param to put ParameterBuilder.ifPresent(encryptor -> { encryptInsertValues((GroupedParameterBuilder) parameterBuilder, insertStatementContext, encryptor, tableName, columnName); }); }}Copy the code

Finally locate the encryptInsertValue method and place the override parameters into the StandardParameterBuilder.

private void encryptInsertValue(final Encryptor encryptor, final String tableName, final int columnIndex,
                                    final Object originalValue, final StandardParameterBuilder parameterBuilder, final String encryptLogicColumnName) {
    // FIXME: can process all part of insert value is ? or literal, can not process mix ? and literal
    // For example: values (? ,?) , (1, 1) can process
    // For example: values (?, 1), (?, 2) can not process
    // Add the encrypted plain text to parameterBuilder
    parameterBuilder.addReplacedParameters(columnIndex, encryptor.encrypt(originalValue));
    Collection<Object> addedParameters = new LinkedList<>();
    // Auxiliary query values are put into parameterBuilder
    if (encryptor instanceof QueryAssistedEncryptor) {
        Optional<String> assistedColumnName = getEncryptRule().findAssistedQueryColumn(tableName, encryptLogicColumnName);
        Preconditions.checkArgument(assistedColumnName.isPresent(), "Can not find assisted query Column Name");
        addedParameters.add(((QueryAssistedEncryptor) encryptor).queryAssistedEncrypt(originalValue.toString()));
    }
    // The plaintext values are put into parameterBuilder
    if (getEncryptRule().findPlainColumn(tableName, encryptLogicColumnName).isPresent()) {
        addedParameters.add(originalValue);
    }
    if(! addedParameters.isEmpty()) {if(! parameterBuilder.getAddedIndexAndParameters().containsKey(columnIndex +1)) {
            parameterBuilder.getAddedIndexAndParameters().put(columnIndex + 1.new LinkedList<>());
        }
        parameterBuilder.getAddedIndexAndParameters().get(columnIndex + 1).addAll(addedParameters); }}Copy the code

As you can see from the source code comments above, mixing literal and param modes into a single row is not supported when encryption rules are configured. For example, with this insertion, it’s a really big hole.

private static void insert(Connection connection, int id, String pwd) throws SQLException {
    String sql = "insert into my_user (id, pwd) values (9998, ?) ";
    PreparedStatement statement = connection.prepareStatement(sql);
    statement.setString(1, String.valueOf(System.currentTimeMillis()));
    statement.execute();
    statement.close();
}
Copy the code

Problem is StandardParameterBuilder# getParameters this method executes, Java. Proceedings of the lang. IndexOutOfBoundsException anomalies.

Sqlrouterewriteengineer #rewrite (AbstractSQLBuilder#toSQL) Call the StandardParameterBuilder#getParameters method when you get the param list overridden.

@RequiredArgsConstructor
public final class StandardParameterBuilder implements ParameterBuilder {
    // Raw argument list
    private final List<Object> originalParameters;
    // index - The parameter to be added
    @Getter
    private final Map<Integer, Collection<Object>> addedIndexAndParameters = new TreeMap<>();
    // index - The parameter to be replaced
    private final Map<Integer, Object> replacedIndexAndParameters = new LinkedHashMap<>();
    // The index to remove
    private final List<Integer> removeIndexAndParameters = new ArrayList<>();
    // To be exposed to the outside
    public void addAddedParameters(final int index, final Collection<Object> parameters) {
        addedIndexAndParameters.put(index, parameters);
    }
    // To be exposed to the outside
    public void addReplacedParameters(final int index, final Object parameter) {
        replacedIndexAndParameters.put(index, parameter);
    }
    // To be exposed to the outside
    public void addRemovedParameters(final int index) {
        removeIndexAndParameters.add(index);
    }
    @Override
    public List<Object> getParameters(a) {
        List<Object> result = new LinkedList<>(originalParameters);
        // Replace the value of the subscript
        for (Entry<Integer, Object> entry : replacedIndexAndParameters.entrySet()) {
            // IndexOutOfBoundsException
            result.set(entry.getKey(), entry.getValue());
        }
        // Add the value of the subscript
        for (Entry<Integer, Collection<Object>> entry : ((TreeMap<Integer, Collection<Object>>) addedIndexAndParameters).descendingMap().entrySet()) {
            if (entry.getKey() > result.size()) {
                result.addAll(entry.getValue());
            } else{ result.addAll(entry.getKey(), entry.getValue()); }}// Remove the subscript value
        for (int index : removeIndexAndParameters) {
            result.remove(index);
        }
        returnresult; }}Copy the code

2. SQLToken is replaced

SQLToken EncryptInsertValuesTokenGenerator create encrypted insert. Nothing special, just the (? ,?) (instead? ,? ,?) .

 @Override
public InsertValuesToken generateSQLToken(final InsertStatementContext insertStatementContext) {
    Optional<SQLToken> insertValuesToken = findPreviousSQLToken(InsertValuesToken.class);
    if (insertValuesToken.isPresent()) {
        processPreviousSQLToken(insertStatementContext, (InsertValuesToken) insertValuesToken.get());
        return (InsertValuesToken) insertValuesToken.get();
    }
    return generateNewSQLToken(insertStatementContext);
}

// Core logic
private void encryptToken(final InsertValue insertValueToken, final String tableName, final InsertStatementContext insertStatementContext, final InsertValueContext insertValueContext) {
    Optional<SQLToken> useDefaultInsertColumnsToken = findPreviousSQLToken(UseDefaultInsertColumnsToken.class);
    Iterator<String> descendingColumnNames = insertStatementContext.getDescendingColumnNames();
    while (descendingColumnNames.hasNext()) {
        String columnName = descendingColumnNames.next();
        // Find the corresponding Encryptor to the logical column name
        Optional<Encryptor> encryptor = getEncryptRule().findEncryptor(tableName, columnName);
        if (encryptor.isPresent()) {
            int columnIndex = useDefaultInsertColumnsToken.map(sqlToken -> ((UseDefaultInsertColumnsToken) sqlToken).getColumns().indexOf(columnName))
                    .orElseGet(() -> insertStatementContext.getColumnNames().indexOf(columnName));
            / / clear
            Object originalValue = insertValueContext.getValue(columnIndex);
            // Add the original column
            addPlainColumn(insertValueToken, columnIndex, tableName, columnName, insertValueContext, originalValue);
            // Add secondary query columns
            addAssistedQueryColumn(insertValueToken, encryptor.get(), columnIndex, tableName, columnName, insertValueContext, originalValue);
            // Set the ciphertext columnsetCipherColumn(insertValueToken, encryptor.get(), columnIndex, insertValueContext.getValueExpressions().get(columnIndex), originalValue); }}}Copy the code

Historical data update

Sharding-sphere does not provide desensitization schemes with historical data. The pwd_ENCRYPT field is always empty and requires script initialization.

When the new data is inserted, it is encrypted into ciphertext data through Apache ShardingSphere and stored in the cipherColumn. Now you need to deal with the historical cleartext stock data. Since Apache ShardingSphere does not provide relevant data migration and washing tools, the service needs to encrypt the plaintext data in the PWD and store it in the PWd_cipher.

Query. With. Cipher. Column =true

Old user

The query

Logic SQL: select * from my_user where id = ? 
Actual SQL: ds_1 ::: select id, pwd_encrypt AS pwd from my_user_1 where id = ? ::: [3] 

Logic SQL: select id, pwd from my_user where id = ? 
Actual SQL: ds_1 ::: select id, pwd_encrypt AS pwd from my_user_1 where id = ? ::: [3] 

Logic SQL: select * from my_user where pwd = ? 
Actual SQL: ds_0 ::: select id, pwd_encrypt AS pwd from my_user_0 where pwd_encrypt = ? ::: [/sRcoTnWHoY1+PT9F6tpAg==] 
Actual SQL: ds_0 ::: select id, pwd_encrypt AS pwd from my_user_1 where pwd_encrypt = ? ::: [/sRcoTnWHoY1+PT9F6tpAg==] 
Actual SQL: ds_1 ::: select id, pwd_encrypt AS pwd from my_user_0 where pwd_encrypt = ? ::: [/sRcoTnWHoY1+PT9F6tpAg==] 
Actual SQL: ds_1 ::: select id, pwd_encrypt AS pwd from my_user_1 where pwd_encrypt = ? ::: [/sRcoTnWHoY1+PT9F6tpAg==] 
Copy the code

1. The query field is overwritten pwd_encrypt AS PWD

The reason and the front before the migration, see EncryptProjectionTokenGenerator# getEncryptColumnName, due to the query. With. Cipher. The column = true will return here pwd_encrypt cipher text field.

private String getEncryptColumnName(final String tableName, final String logicEncryptColumnName) {
  // Locate the original column according to the logicEncryptColumnName logical column name
  Optional<String> plainColumn = getEncryptRule().findPlainColumn(tableName, logicEncryptColumnName);
  // If the original field exists and query.with.cipher.column=false, the original field is returned
  // If the original field does not exist or query.with.cipher.column=true, the ciphertext field is returned
  returnplainColumn.isPresent() && ! queryWithCipherColumn ? plainColumn.get() : getEncryptRule().getCipherColumn(tableName, logicEncryptColumnName); }Copy the code

Where pwd_encrypt =?

First founded SQLToken EncryptPredicateColumnTokenGenerator.

private Collection<SubstitutableColumnNameToken> generateSQLTokens(final SQLStatementContext sqlStatementContext, final AndPredicate andPredicate) {
  Collection<SubstitutableColumnNameToken> result = new LinkedList<>();
  for (PredicateSegment each : andPredicate.getPredicates()) {
      Optional<EncryptTable> encryptTable = findEncryptTable(sqlStatementContext, each);
      if(! encryptTable.isPresent() || ! encryptTable.get().findEncryptor(each.getColumn().getIdentifier().getValue()).isPresent()) {continue;
      }
      int startIndex = each.getColumn().getOwner().isPresent() ? each.getColumn().getOwner().get().getStopIndex() + 2 : each.getColumn().getStartIndex();
      int stopIndex = each.getColumn().getStopIndex();
      // query.with.cipher.column=false
      if(! queryWithCipherColumn) { Optional<String> plainColumn = encryptTable.get().findPlainColumn(each.getColumn().getIdentifier().getValue());if (plainColumn.isPresent()) {
              result.add(new SubstitutableColumnNameToken(startIndex, stopIndex, plainColumn.get()));
              continue; }}// query.with.cipher.column=true
      // Determine whether the secondary query column matches the current PredicateSegment assertion. If it is a secondary query column, the secondary query column is returned
      Optional<String> assistedQueryColumn = encryptTable.get().findAssistedQueryColumn(each.getColumn().getIdentifier().getValue());
      SubstitutableColumnNameToken encryptColumnNameToken = assistedQueryColumn.map(columnName -> new SubstitutableColumnNameToken(startIndex, stopIndex, columnName))
              // Otherwise return the ciphertext column
              .orElseGet(() -> new SubstitutableColumnNameToken(startIndex, stopIndex, encryptTable.get().getCipherColumn(each.getColumn().getIdentifier().getValue())));
      result.add(encryptColumnNameToken);
  }
  return result;
}
Copy the code

Secondly EncryptPredicateParameterRewriter will rewrite the query parameter for the cipher text.

@Setter
public final class EncryptPredicateParameterRewriter extends EncryptParameterRewriter<SQLStatementContext> implements SchemaMetaDataAware.QueryWithCipherColumnAware {
  private SchemaMetaData schemaMetaData;
  private boolean queryWithCipherColumn;
  @Override
  public void rewrite(final ParameterBuilder parameterBuilder, final SQLStatementContext sqlStatementContext, final List<Object> parameters) {
      // Create an EncryptCondition, similar to ShardingCondition, that represents an = or in assertion for a field in a table
      List<EncryptCondition> encryptConditions = new EncryptConditionEngine(getEncryptRule(), schemaMetaData).createEncryptConditions(sqlStatementContext);
      if (encryptConditions.isEmpty()) {
          return;
      }
      for (EncryptCondition each : encryptConditions) {
          if (queryWithCipherColumn) {
              // Encrypt the raw query parameters
              List<Object> encryptedValues = getEncryptedValues(each, each.getValues(parameters));
              // parameterBuilder replaces query parametersencryptParameters(parameterBuilder, each.getPositionIndexMap(), encryptedValues); }}}}Copy the code

EncryptCondition #createEncryptCondition creates an EncryptCondition. and… Comparison.

private Optional<EncryptCondition> createEncryptCondition(final PredicateSegment predicateSegment, final String tableName) {
  // Compare operators such as <>! = =
  if (predicateSegment.getRightValue() instanceof PredicateCompareRightValue) {
      PredicateCompareRightValue compareRightValue = (PredicateCompareRightValue) predicateSegment.getRightValue();
      return isSupportedOperator(compareRightValue.getOperator()) ? createCompareEncryptCondition(tableName, predicateSegment, compareRightValue) : Optional.empty();
  }
  // in
  if (predicateSegment.getRightValue() instanceof PredicateInRightValue) {
      return createInEncryptCondition(tableName, predicateSegment, (PredicateInRightValue) predicateSegment.getRightValue());
  }
   // Between and is not supported
  if (predicateSegment.getRightValue() instanceof PredicateBetweenRightValue) {
      throw new ShardingSphereException("The SQL clause 'BETWEEN... AND... ' is unsupported in encrypt rule.");
  }
  return Optional.empty();
}
Copy the code

EncryptPredicateParameterRewriter# getEncryptedValues prefer using auxiliary query value as a condition of the query.

private List<Object> getEncryptedValues(final EncryptCondition encryptCondition, final List<Object> originalValues) {
  String tableName = encryptCondition.getTableName();
  String columnName = encryptCondition.getColumnName();
  return getEncryptRule().findAssistedQueryColumn(tableName, columnName).isPresent()
          // If the secondary query exists, use the secondary query value
          ? getEncryptRule().getEncryptAssistedQueryValues(tableName, columnName, originalValues)
          // Otherwise use encrypted values
          : getEncryptRule().getEncryptValues(tableName, columnName, originalValues);
}
Copy the code

EncryptPredicateParameterRewriter# encryptParameters, will rewrite the parameters to pass into StandardParameterBuilder addReplacedParameters method.

private void encryptParameters(final ParameterBuilder parameterBuilder, final Map<Integer, Integer> positionIndexes, final List<Object> encryptValues) {
    if(! positionIndexes.isEmpty()) {for(Entry<Integer, Integer> entry : positionIndexes.entrySet()) { ((StandardParameterBuilder) parameterBuilder).addReplacedParameters(entry.getValue(), encryptValues.get(entry.getKey())); }}}Copy the code

update

Logic SQL: update my_user set pwd = ? where id = ? Actual SQL: ds_1 ::: update my_user_1 set pwd_encrypt = ? , pwd = ? where id = ? ::: [b9ValhsVZfJ2Mu7GKmsHag==, 1609060586708, 3]Copy the code

And open the query with. Cipher. The column as before, create SQLToken EncryptAssignmentTokenGenerator, EncryptAssignmentParameterRewriter encryption parameter has been added, SQL has not changed.

A new user

new

Logic SQL: insert into my_user (id, pwd) values (?, ?) 
Actual SQL: ds_0 ::: insert into my_user_0 (id, pwd_encrypt, pwd) values (?, ?, ?) ::: [5140, NHj62xHlEngXKI9YV7twyg==, 1609061001092] 
Copy the code

And open the query with. Cipher. The column before, EncryptInsertValueParameterRewriter new encryption parameters, create SQLToken EncryptInsertValuesTokenGenerator.

4. After migration

The plaintext column was deleted

Pay attention to the first argument here EncryptColumnRuleConfiguration configuration, originally is the PWD represents the text of the actual field data table, this time.

private EncryptRuleConfiguration createEncryptRuleConfig2(a) {
  EncryptRuleConfiguration configuration = new EncryptRuleConfiguration();
  // Encrypt and decrypt
  Properties properties = new Properties();
  properties.setProperty("aes.key.value"."123456");
  EncryptorRuleConfiguration aesRuleConfiguration = new EncryptorRuleConfiguration("aes", properties);
  configuration.getEncryptors().put("user_pwd_encryptor", aesRuleConfiguration);
  The pwdEncryptConfig field PWD has been removed
  EncryptColumnRuleConfiguration pwdEncryptConfig = new EncryptColumnRuleConfiguration(null."pwd_encrypt".""."user_pwd_encryptor");
  Map<String, EncryptColumnRuleConfiguration> columns = new HashMap<>();
  columns.put("pwd", pwdEncryptConfig);// Logical field - encryption configuration
  configuration.getTables().put("my_user".new EncryptTableRuleConfiguration(columns));
  return configuration;
}
Copy the code

DDL deletes plaintext fields

private static void dropPlainPwdColumn(Connection connection) throws SQLException {
  String sql = "alter table my_user drop column pwd";
  Statement statement = connection.createStatement();
  statement.execute(sql);
  statement.close();
}
Copy the code

To add to check

Logic SQL: select * from my_user where id = ? 
Actual SQL: ds_1 ::: select id, pwd_encrypt AS pwd from my_user_1 where id = ? ::: [3] 

Logic SQL: select id, pwd from my_user where id = ? 
Actual SQL: ds_1 ::: select id, pwd_encrypt AS pwd from my_user_1 where id = ? ::: [3] 

Logic SQL: update my_user set pwd = ? where id = ? 
Actual SQL: ds_1 ::: update my_user_1 set pwd_encrypt = ? where id = ? ::: [4tzE6aM2SQXhj8sJcJrjmQ==, 3] 
Logic SQL: select * from my_user where pwd = ? 
Actual SQL: ds_0 ::: select id, pwd_encrypt AS pwd from my_user_0 where pwd_encrypt = ? ::: [4tzE6aM2SQXhj8sJcJrjmQ==] 
Actual SQL: ds_0 ::: select id, pwd_encrypt AS pwd from my_user_1 where pwd_encrypt = ? ::: [4tzE6aM2SQXhj8sJcJrjmQ==] 
Actual SQL: ds_1 ::: select id, pwd_encrypt AS pwd from my_user_0 where pwd_encrypt = ? ::: [4tzE6aM2SQXhj8sJcJrjmQ==] 
Actual SQL: ds_1 ::: select id, pwd_encrypt AS pwd from my_user_1 where pwd_encrypt = ? ::: [4tzE6aM2SQXhj8sJcJrjmQ==] 

Logic SQL: insert into my_user (id, pwd) values (?, ?) 
Actual SQL: ds_1 ::: insert into my_user_1 (id, pwd_encrypt) values (?, ?) ::: [9343, enAqjGiquCRS7lhheLgmnA==] 

Copy the code

There is no change for SELECT.

For insert, such as EncryptInsertValuesTokenGenerator# addPlainColumn, when creating SQLToken judgment field configuration does not exist, the original will not generate SQLToken text field.

private void addPlainColumn(final InsertValue insertValueToken, final int columnIndex,
                          final String tableName, final String columnName, final InsertValueContext insertValueContext, final Object originalValue) {
  if (getEncryptRule().findPlainColumn(tableName, columnName).isPresent()) {
      DerivedSimpleExpressionSegment derivedExpressionSegment = insertValueContext.getParameters().isEmpty()
              ? new DerivedLiteralExpressionSegment(originalValue) : new DerivedParameterMarkerExpressionSegment(getParameterIndexCount(insertValueToken));
      insertValueToken.getValues().add(columnIndex + 1, derivedExpressionSegment); }}Copy the code

Similarly, EncryptInsertValueParameterRewriter# encryptInsertValue rewrite the argument list, also because I couldn’t find the original field configuration, not the original parameters in the parameter list.

private void encryptInsertValue(final Encryptor encryptor, final String tableName, final int columnIndex,
                                final Object originalValue, final StandardParameterBuilder parameterBuilder, final String encryptLogicColumnName) {
    // Add the encrypted plain text to parameterBuilder
    parameterBuilder.addReplacedParameters(columnIndex, encryptor.encrypt(originalValue));
    Collection<Object> addedParameters = new LinkedList<>();
    // Auxiliary query values are put into parameterBuilder
    if (encryptor instanceof QueryAssistedEncryptor) {
        Optional<String> assistedColumnName = getEncryptRule().findAssistedQueryColumn(tableName, encryptLogicColumnName);
        Preconditions.checkArgument(assistedColumnName.isPresent(), "Can not find assisted query Column Name");
        addedParameters.add(((QueryAssistedEncryptor) encryptor).queryAssistedEncrypt(originalValue.toString()));
    }
    // The result set will not be added
    if (getEncryptRule().findPlainColumn(tableName, encryptLogicColumnName).isPresent()) {
        addedParameters.add(originalValue);
    }
    if(! addedParameters.isEmpty()) {if(! parameterBuilder.getAddedIndexAndParameters().containsKey(columnIndex +1)) {
            parameterBuilder.getAddedIndexAndParameters().put(columnIndex + 1.new LinkedList<>());
        }
        parameterBuilder.getAddedIndexAndParameters().get(columnIndex + 1).addAll(addedParameters); }}Copy the code

For the same is true for the update, see EncryptAssignmentTokenGenerator# addPlainColumn and EncryptAssignmentParameterRewriter# encryptParameters.

4. Auxiliary query column

The secondary query column has been mentioned above, why do you need this field? The main need is this:

Even if the same data, such as two users with the same password, the encrypted data stored in the database should be different. This concept is more conducive to the protection of user information, prevent the success of the collision library.

In response to this need, Org. Apache. Shardingsphere. Encrypt. Strategy. Spi. The Encryptor a subinterface org. Apache. Shardingsphere. Encrypt. Strategy. The spi. QueryAssist EdEncryptor provides a secondary query method that converts plaintext values into secondary query values to query secondary query fields. Just implement an Encryptor that is guaranteed to be reversible and the same original ciphertext is different.

public interface QueryAssistedEncryptor extends Encryptor {
    String queryAssistedEncrypt(String plaintext);
}
Copy the code

It provides three functions for implementation, namely encrypt(), decrypt(), and queryAssistedEncrypt(). In the ENCRYPT () phase, the user sets some variation seed, such as a timestamp. Encrypting the contents of the combination of original data and variable seeds can ensure that even if the original data is the same, the encrypted data is not the same because of the existence of variable seeds. In Decrypt (), the seed data is decrypted using the previously specified encryption algorithm.

Because queryAssistedEncrypt() and encrypt() produce different encrypted data for storage, decrypt() is reversible and queryAssistedEncrypt() is irreversible. When querying raw data, we automatically parse, rewrite, and route SQL, use secondary query columns to query WHERE conditions, and use decrypt() to decrypt the encrypted () data and return the raw data to the user. All of this is transparent to the user.

The following is an example implemented according to the official document. AES encryption method with time stamps random seeds ensures that database data is different. AES decryption removes random seeds and returns them to the user.

QueryAssistedEncryptor implements the following, remember to inject via SPI under meta-info /services:

public class CustomQueryAssistedEncryptor implements QueryAssistedEncryptor {
	// Delegate AES to perform encrypt and decrypt
    private final Encryptor aesEncryptor;
    // Delegate MD5 to queryAssistedEncrypt
    private final Encryptor digestEncryptor;
    private Properties properties;
	// Random seed length
    private final int seedLength = String.valueOf(System.currentTimeMillis()).length();

    public CustomQueryAssistedEncryptor(a) {
        this.aesEncryptor = new AESEncryptor();
        this.digestEncryptor = new MD5Encryptor();
    }

    @Override
    public String queryAssistedEncrypt(String plaintext) {
        String salt = this.getProperties().getProperty("md5.salt");
        return digestEncryptor.encrypt(plaintext + salt);
    }

    @Override
    public void init(a) {}@Override
    public String encrypt(Object plaintext) {
        return aesEncryptor.encrypt(plaintext + String.valueOf(System.currentTimeMillis()));
    }

    @Override
    public Object decrypt(String ciphertext) {
        String decrypt = (String) aesEncryptor.decrypt(ciphertext);
        return decrypt.substring(0, decrypt.length() - seedLength);
    }

    @Override
    public String getType(a) {
        return "CustomQueryAssistedEncryptor";
    }

    @Override
    public Properties getProperties(a) {
        return this.properties;
    }

    @Override
    public void setProperties(Properties properties) {
        this.properties = properties; aesEncryptor.setProperties(properties); digestEncryptor.setProperties(properties); }}Copy the code

The encryption configuration is as follows:

 private EncryptRuleConfiguration createEncryptRuleConfig(a) {
    EncryptRuleConfiguration configuration = new EncryptRuleConfiguration();
    // Encrypt and decrypt
    Properties properties = new Properties();
    properties.setProperty("aes.key.value"."123456");
    properties.setProperty("md5.salt"."abcdef");
    EncryptorRuleConfiguration aesRuleConfiguration = new EncryptorRuleConfiguration("CustomQueryAssistedEncryptor", properties);
    configuration.getEncryptors().put("user_pwd_encryptor", aesRuleConfiguration);
    // table - field - encryption rules
    EncryptColumnRuleConfiguration encryptConfig = new EncryptColumnRuleConfiguration(null."pwd_encrypt"."pwd_assist"."user_pwd_encryptor");
    Map<String, EncryptColumnRuleConfiguration> columns = new HashMap<>();
    columns.put("pwd", encryptConfig);// Logical field - encryption configuration
    configuration.getTables().put("my_user_assist".new EncryptTableRuleConfiguration(columns));
    return configuration;
}
Copy the code

Corresponding to the DDL:

create table my_user_assist (id bigint(20) not null primary key, pwd_encrypt varchar(255) not null, pwd_assist varchar(255) not null)
Copy the code

Increase, change, check and rewrite results:

Logic SQL: insert into my_user_assist (id, PWD) values (? ,?) Actual SQL: ds_1 ::: insert into my_user_assist_1 (id, pwd_encrypt, pwd_assist) values (? ,? ,?) ::: [3, 1d/2wa8LM9FlepSZPmy3EC9HI2/xGzx9sq0qEBVSvC4=, 6f3b8ded65bd7a4db11625ac84e579bb] Logic SQL: insert into my_user_assist (id, pwd) values (? ,?) Actual SQL: ds_0 ::: insert into my_user_assist_0 (id, pwd_encrypt, pwd_assist) values (? ,? ,?) ::: [4, NPdqE1U22vN2awqLfcQGVmq90U7dDOEve/JY/7PbzCQ=, 6f3b8ded65bd7a4db11625ac84e579bb] Logic SQL: select * from my_user_assist where id = ? Actual SQL: ds_1 ::: select id, pwd_encrypt AS pwd from my_user_assist_1 where id = ? ::: [3] Logic SQL: select id, pwd from my_user_assist where id = ? Actual SQL: ds_1 ::: select id, pwd_encrypt AS pwd from my_user_assist_1 where id = ? ::: [3] Logic SQL: update my_user_assist set pwd = ? where id = ? Actual SQL: ds_1 ::: update my_user_assist_1 set pwd_encrypt = ? , pwd_assist = ? where id = ? ::: [4KpzieXQFNUpjdBToqGmYt1os93mcmEDK2zLftdNxIE=, 6f3b8ded65bd7a4db11625ac84e579bb, 3] Logic SQL: select * from my_user_assist where pwd = ? Actual SQL: ds_0 ::: select id, pwd_encrypt AS pwd from my_user_assist_0 where pwd_assist = ? ::: [6f3b8ded65bd7a4db11625ac84e579bb] Actual SQL: ds_0 ::: select id, pwd_encrypt AS pwd from my_user_assist_1 where pwd_assist = ? ::: [6f3b8ded65bd7a4db11625ac84e579bb] Actual SQL: ds_1 ::: select id, pwd_encrypt AS pwd from my_user_assist_0 where pwd_assist = ? ::: [6f3b8ded65bd7a4db11625ac84e579bb] Actual SQL: ds_1 ::: select id, pwd_encrypt AS pwd from my_user_assist_1 where pwd_assist = ? ::: [6f3b8ded65bd7a4db11625ac84e579bb]Copy the code

conclusion

  • The main logic for sharding-jdbc data desensitization points is focused on the SQL rewrite step, and on the first and second steps of the SQLRewriteEntry#createSQLRewriteContext methods.

    • The first step, performed by the EncryptSQLRewriteContextDecorator ParameterRewriter rewrite the Parameter.
    • The second step, founded by EncryptSQLRewriteContextDecorator SQLTokenGenerator, create SQLToken.
  • Query.with.cipher. column indicates whether ciphertext query is enabled. If yes, the SELECT field needs to be queried and decrypted, and the WHERE condition needs to be encrypted before query.

  • One pitfall to note is that whenever you configure an EncryptRuleConfiguration encryption configuration, ensure that inserts are either full of question mark placeholders or dead values. Insert into table (id, PWD, deleted) values (? ,? , 0), otherwise an error will be reported.

  • Auxiliary query columns. If the same plain text needs to generate a different ciphertext database, and you need to use the logical column to perform where conditional query, you need to implement the QueryAssistedEncryptor interface.