Best practices for Springboot configuration files and privacy data desensitization (principle + source code)

Hello everyone! I am Xiaofu~

In the past few days, the company has been investigating the leakage of internal data accounts. The reason is that it was found that some intern Xiaokai actually privately uploaded the source code with the account number and password GitHub, resulting in the leakage of core data, and the child has not been beaten by society. This kind of thing The consequences can be big or small.

Speaking of this, I feel more emotional. Before my experience of TM being deleted from the database , I still feel uncomfortable when I think about it. I also submitted the plaintext password of the database account by mistake GitHub, and then the big baby deleted it from the test database for me. Later, I remembered and encrypted the contents of the configuration file. The data security problem should not be underestimated. Regardless of work or life, sensitive data must be desensitized.

If you are not familiar with the concept of desensitization, you can take a look at the 6 data desensitization schemes I wrote before, which are also used by large factories. The desensitization is briefly described in the article. The following two are more common in the sharing work. desensitization scene.

Configure desensitization

To realize the desensitization of the configuration, I used Javaan encryption and decryption tool Jasypt, which provides 单密钥对称加密and 非对称加密two desensitization methods.

Single-key symmetric encryption: A key is added with salt, which can be used as the basis for both encryption and decryption of content;

Asymmetric encryption: Only use the public key and the private key to encrypt and decrypt the content;

The above two encryption methods are very simple to use. Let's take the springbootintegrated single-key symmetric encryption method as an example.

First import the jasypt-spring-boot-starterjar

 <!--配置文件加密-->
 <dependency>
     <groupId>com.github.ulisesbocchio</groupId>
     <artifactId>jasypt-spring-boot-starter</artifactId>
     <version>2.1.0</version>
 </dependency>

The configuration file adds the key configuration item jasypt.encryptor.password, and replaces the value that needs to be desensitized valuewith the pre-encrypted content ENC(mVTvp4IddqdaYGqPl9lCQbzM3H/b0B6l).

We can define this format at will. For example, if we want the abc[mVTvp4IddqdaYGqPl9lCQbzM3H/b0B6l]format, we only need to configure the prefix and suffix.

jasypt:
  encryptor:
    property:
      prefix: "abc["
      suffix: "]"

The ENC(XXX) format is mainly for the convenience of identifying whether the value needs to jasyptbe decrypted.

spring:
  datasource:
    url: jdbc:mysql://1.2.3.4:3306/xiaofu?useSSL=false&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&ze oDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
    username: xiaofu
    password: ENC(mVTvp4IddqdaYGqPl9lCQbzM3H/b0B6l)

# 秘钥
jasypt:
  encryptor:
    password: 程序员内点事(然而不支持中文)

The secret key is an attribute with high security requirements, so it is generally not recommended to put it directly in the project. It can be -Dinjected through the parameters at startup, or placed in the configuration center to avoid leakage.

java -jar -Djasypt.encryptor.password=1123  springboot-jasypt-2.3.3.RELEASE.jar

Pre-generated encrypted values, which can be generated by in-code API calls

@Autowired
private StringEncryptor stringEncryptor;

public void encrypt(String content) {
    String encryptStr = stringEncryptor.encrypt(content);
    System.out.println("加密后的内容:" + encryptStr);
}

Or generated by the following Java command, several parameters D:\maven_lib\org\jasypt\jasypt\1.9.3\jasypt-1.9.3.jarare the jasypt core jar package, inputthe text to be encrypted, passwordthe secret key, and algorithmthe encryption algorithm used.

java -cp  D:\maven_lib\org\jasypt\jasypt\1.9.3\jasypt-1.9.3.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI input="root" password=xiaofu  algorithm=PBEWithMD5AndDES

If it can start normally after one operation, it means that the desensitization of the configuration file is no problem.

Desensitization of sensitive fields

The private data of users in the production environment, such as mobile phone numbers, ID cards, or some account configuration information, must be desensitized without landing when entering the warehouse, that is, real-time desensitization is required when entering our system.

User data enters the system, desensitized and persisted to the database, and reverse decryption is required when users query data. This kind of scenario generally requires global processing, so it is not AOPsuitable to use the aspect to achieve.

First, customize two annotations @EncryptFieldand @EncryptMethoduse them on field attributes and methods respectively. The implementation idea is very simple. As long as the method is applied to the @EncryptMethodannotation, check whether the input field is annotated @EncryptFieldwith annotation, and encrypt the corresponding field content if there is.

@Documented
@Target({ElementType.FIELD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptField {

    String[] value() default "";
}
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptMethod {

    String type() default ENCRYPT;
}

The implementation of the aspect is also relatively simple, encrypting the entry and decrypting the returned result. For the convenience of reading, only part of the code is posted here, and the Github address of the complete case: https://github.com/chengxy-nds/Springboot-Notebook/tree/master/springboot-jasypt

@Slf4j
@Aspect
@Component
public class EncryptHandler {

    @Autowired
    private StringEncryptor stringEncryptor;

    @Pointcut("@annotation(com.xiaofu.annotation.EncryptMethod)")
    public void pointCut() {
    }

    @Around("pointCut()")
    public Object around(ProceedingJoinPoint joinPoint) {
        /**
         * 加密
         */
        encrypt(joinPoint);
        /**
         * 解密
         */
        Object decrypt = decrypt(joinPoint);
        return decrypt;
    }

    public void encrypt(ProceedingJoinPoint joinPoint) {

        try {
            Object[] objects = joinPoint.getArgs();
            if (objects.length != 0) {
                for (Object o : objects) {
                    if (o instanceof String) {
                        encryptValue(o);
                    } else {
                        handler(o, ENCRYPT);
                    }
                    //TODO 其余类型自己看实际情况加
                }
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    public Object decrypt(ProceedingJoinPoint joinPoint) {
        Object result = null;
        try {
            Object obj = joinPoint.proceed();
            if (obj != null) {
                if (obj instanceof String) {
                    decryptValue(obj);
                } else {
                    result = handler(obj, DECRYPT);
                }
                //TODO 其余类型自己看实际情况加
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
        return result;
    }
    。。。
}

Next, to test the effect of facet annotations, we desensitize fields mobileand addressannotations .@EncryptField

@EncryptMethod
@PostMapping(value = "test")
@ResponseBody
public Object testEncrypt(@RequestBody UserVo user, @EncryptField String name) {

    return insertUser(user, name);
}

private UserVo insertUser(UserVo user, String name) {
    System.out.println("加密后的数据:user" + JSON.toJSONString(user));
    return user;
}

@Data
public class UserVo implements Serializable {

    private Long userId;

    @EncryptField
    private String mobile;

    @EncryptField
    private String address;

    private String age;
}

Request this interface and see that the parameters are successfully encrypted, and the data returned to the user is still the data before desensitization, which is in line with our expectations, then this simple desensitization implementation is over.

know why know why

JasyptAlthough the tool is simple and easy to use, as programmers, we can’t just be satisfied with skilled use. It is necessary to understand the underlying implementation principle, which is very important for subsequent debugging of bugs and secondary development of extended functions.

Personally, I think Jasyptthe principle of configuration file desensitization is very simple. It is nothing more than intercepting the operation of obtaining the configuration before using the configuration information, and decrypting the corresponding encrypted configuration before using it.

Is this the case? Let's take a brief look at the implementation of the source code. Since it is springbootintegrated in a way, let's start with the jasypt-spring-boot-startersource code.

starterThere is very little code, and the main work is to inject the classes that need to be preprocessed by SPIregistering services and annotations through the mechanism .@ImportJasyptSpringBootAutoConfiguration

EnableEncryptablePropertiesConfigurationA core processing class is registered in the front-loading class EnableEncryptablePropertiesBeanFactoryPostProcessor.

Its constructor has two parameters, which are ConfigurableEnvironmentused to obtain all the attachment information and EncryptablePropertySourceConverterparse the configuration information.

I found the processing class specifically responsible for decryption EncryptablePropertySourceWrapper. It rewrote the method by extending the Springattribute management class . When obtaining the configuration, all the values ​​wrapped in the specified format such as ENC(x) are all decrypted.PropertySource<T>getProperty(String name)

Now that we know the principle, it will be much easier for us to follow up with secondary development, such as switching encryption algorithms or implementing our own desensitization tools.

Case Github address: https://github.com/chengxy-nds/Springboot-Notebook/tree/master/springboot-jasypt

PBE algorithm

Let's talk about Jasyptthe encryption algorithm used in China. In fact, it is JCE.jarencapsulated on the basis of the JDK package. In essence, it still uses the algorithm provided by the JDK. The default algorithm is used. It is PBEvery PBEWITHMD5ANDDESinteresting to see the name of this algorithm. Look, PBE, WITH, MD5, AND, DES seem to have some stories, keep reading.

PBEAlgorithm ( Password Based Encryption, encryption based on password (password)) is a password-based encryption algorithm, which is characterized in that the password is mastered by the user, and the security of data is ensured by adding random number multiple encryption and other methods.

In essence, the PBE algorithm does not really build a new encryption and decryption algorithm, but wraps the algorithm we already know. For example: commonly used message digest algorithms MD5and SHAalgorithms, symmetric encryption algorithms DES, RC2etc., and the PBEalgorithm is a reasonable combination of these algorithms, which also echoes the name of the previous algorithm.

Since the PBE algorithm uses our more commonly used symmetric encryption algorithm, it will involve the problem of the key. But it does not have the concept of a key itself, only the password, and the key is calculated by the password through the encryption algorithm.

The password itself is not very long, so it cannot be used to replace the key. Only the password can be easily deciphered by brute force attacks. At this time, it is necessary to add some salt .

The salt is usually some random information, such as random numbers and timestamps. The salt is attached to the password, and the algorithm is calculated to increase the difficulty of deciphering.

The tricks in the source code

Simply understand the PBE algorithm, and look back at how Jasyptthe source code implements encryption and decryption.

When encrypting, first instantiate the key factory to generate an eight-bit salt value, which is the generator SecretKeyFactoryused by default .jasypt.encryptor.RandomSaltGenerator

public byte[] encrypt(byte[] message) {
    // 根据指定算法,初始化秘钥工厂
    final SecretKeyFactory factory = SecretKeyFactory.getInstance(algorithm1);
    // 盐值生成器,只选八位
    byte[] salt = saltGenerator.generateSalt(8);
    // 
    final PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, iterations);
    // 盐值、口令生成秘钥
    SecretKey key = factory.generateSecret(keySpec);

    // 构建加密器
    final Cipher cipherEncrypt = Cipher.getInstance(algorithm1);
    cipherEncrypt.init(Cipher.ENCRYPT_MODE, key);
    // 密文头部(盐值)
    byte[] params = cipherEncrypt.getParameters().getEncoded();

    // 调用底层实现加密
    byte[] encryptedMessage = cipherEncrypt.doFinal(message);

    // 组装最终密文内容并分配内存(盐值+密文)
    return ByteBuffer
            .allocate(1 + params.length + encryptedMessage.length)
            .put((byte) params.length)
            .put(params)
            .put(encryptedMessage)
            .array();
}

Since the random salt value generator is used by default, the encrypted content of the same content is different each time .

So how to correspond when decrypting it?

Looking at the source code above, it is found that the final encrypted text is composed of two parts. The paramsmessage header contains the password, the randomly generated salt value, and encryptedMessagethe ciphertext.

encryption

encryptedMessageWhen decrypting, the content will be disassembled according to the content of the ciphertext to paramsparse the salt value and password, and the actual content will be decrypted by calling the JDK underlying algorithm.

@Override
@SneakyThrows
public byte[] decrypt(byte[] encryptedMessage) {
    // 获取密文头部内容
    int paramsLength = Byte.toUnsignedInt(encryptedMessage[0]);
    // 获取密文内容
    int messageLength = encryptedMessage.length - paramsLength - 1;
    byte[] params = new byte[paramsLength];
    byte[] message = new byte[messageLength];
    System.arraycopy(encryptedMessage, 1, params, 0, paramsLength);
    System.arraycopy(encryptedMessage, paramsLength + 1, message, 0, messageLength);

    // 初始化秘钥工厂
    final SecretKeyFactory factory = SecretKeyFactory.getInstance(algorithm1);
    final PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray());
    SecretKey key = factory.generateSecret(keySpec);

    // 构建头部盐值口令参数
    AlgorithmParameters algorithmParameters = AlgorithmParameters.getInstance(algorithm1);
    algorithmParameters.init(params);

    // 构建加密器,调用底层算法
    final Cipher cipherDecrypt = Cipher.getInstance(algorithm1);
    cipherDecrypt.init(
            Cipher.DECRYPT_MODE,
            key,
            algorithmParameters
    );
    return cipherDecrypt.doFinal(message);
}

decrypt

I'm Xiaofu, see you next time~

Hundreds of various technical e-books have been sorted out, and students in need can pick them up. The technical group is almost full, and students who want to join can add me as friends, and let’s talk about technology with the big guys.

ebook address

Personal public number: Programmer's internal affairs , welcome to communicate

{{o.name}}
{{m.name}}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324129795&siteId=291194637