Best practices for spring boot profiles, privacy data desensitization!

Posted by ranjita on Tue, 21 Dec 2021 00:57:07 +0100

The company is investigating the internal data account leakage these days. The reason is that it is found that some interns xiaocute secretly transmitted the source code to GitHub with their account and password, resulting in the leakage of core data. The children still haven't been severely beaten by the society. The consequences of this can be large or small.

Speaking of this, I have some feelings. I still feel bad when I think about my experience of TM being deleted. I also mistakenly submitted the plaintext password of the database account to GitHub, and then was deleted by a big baby to my test library. Later, I have a long memory and encrypted the contents of the configuration file. The problem of data security can not be underestimated, whether in 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 six data desensitization schemes that are also used by large factories, which briefly describes desensitization. Next, share two common desensitization scenarios in your work.

Configure desensitization

To realize configuration desensitization, I used Jasypt, an encryption and decryption tool in Java, which provides two desensitization methods: single key symmetric encryption and asymmetric encryption.

Single key symmetric encryption: one key encryption can be used as the basis for content encryption and decryption at the same time;

Asymmetric encryption: the content can be encrypted and decrypted only by using public key and private key;

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

First introduce
jasypt-spring-boot-starter jar

 <!--Profile encryption-->
 <dependency>
     <groupId>com.github.ulisesbocchio</groupId>
     <artifactId>jasypt-spring-boot-starter</artifactId>
     <version>2.1.0</version>
 </dependency>

The configuration file adds the secret key configuration item jasypt encryptor. Password, and replace the value value to be desensitized with the pre encrypted content enc(
mVTvp4IddqdaYGqPl9lCQbzM3H/b0B6l).

We can define this format at will, such as abc[
mVTvp4IddqdaYGqPl9lCQbzM3H/b0B6l] format, just configure prefix and suffix.

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

ENC(XXX) format is mainly used to identify whether the value needs to be decrypted. If it is not configured according to this format, jasypt will keep the original value and will not decrypt when loading the configuration item.

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)

# Secret key
jasypt:
  encryptor:
    password: Something inside the programmer(However, Chinese is not supported)

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 injected through the - D parameter at startup or placed in the configuration center to avoid disclosure.

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

The pre generated encrypted value can be generated by calling the API in the code

@Autowired
private StringEncryptor stringEncryptor;

public void encrypt(String content) {
    String encryptStr = stringEncryptor.encrypt(content);
    System.out.println("Encrypted content:" + encryptStr);
}

Or it can be generated through the following Java command, with several parameters D:\maven_lib\org\jasypt\jasypt .9.3\jasypt-1.9.3.jar is the jasypt core jar package, input text to be encrypted, password secret key and algorithm are the encryption algorithms 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 be started normally after a meal, it means that the configuration file desensitization is no problem.

Sensitive field desensitization

The privacy data of users in the production environment, such as mobile phone number, ID card or some account configuration, shall be desensitized in real time when entering our system.

The user data enters the system and is persisted to the database after desensitization. When the user queries the data, it also needs to be reverse decrypted. This scenario generally requires global processing, so it is most suitable to implement it with AOP aspect.

First, customize two annotations @ EncryptField and @ EncryptMethod, which are respectively used for field properties and methods. The implementation idea is very simple. As long as the @ EncryptMethod annotation is applied to the method, check whether the input parameter field is marked with @ EncryptField annotation, and if so, encrypt the content of the corresponding field.

@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 section is also relatively simple. The input parameters are encrypted and the returned results are decrypted. For ease of reading, only part of the code is posted here. The complete case Github address:
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) {
        /**
         * encryption
         */
        encrypt(joinPoint);
        /**
         * decrypt
         */
        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);
                    }
                    //Other types of TODO can be added according to the actual situation
                }
            }
        } 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);
                }
                //Other types of TODO can be added according to the actual situation
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
        return result;
    }
    . . . 
}

Then test the effect of section annotation. We desensitize the fields mobile and address with the annotation @ 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("Encrypted data: 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;
}

After requesting this interface, you can see that the parameters have been 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 will be completed.

Know what it is, know why

Although Jasypt tool is simple and easy to use, as programmers, we can't just be satisfied with skilled use. It's still necessary to understand the underlying implementation principle, which is very important for subsequent debugging bugs and secondary development extension functions.

Personally, I think the principle of Jasypt profile desensitization is very simple. It is nothing more than intercepting the operation of obtaining the configuration before using the configuration information, decrypting the corresponding encryption configuration and then using it.

Specifically, let's take a simple look at the implementation of the source code. Since it is integrated in the way of springboot, let's start with
Start with the jasypt spring boot starter source code.

There is little starter code. The main work is to register services through SPI mechanism and inject pre processed classes through @ Import annotation
JasyptSpringBootAutoConfiguration.

Load class before
A core processing class is registered in enableencrypttablepropertiesconfiguration
EnableEncryptablePropertiesBeanFactoryPostProcessor.

Its constructor has two parameters. ConfigurableEnvironment is used to obtain all allocation information,
EncryptablePropertySourceConverter parses the configuration information.

Find the processing class responsible for decryption
EncryptablePropertySourceWrapper extends the Spring property management class propertysource < T > and rewrites the getProperty(String name) method. When obtaining the configuration, all values in the specified format such as ENC(x) package are decrypted.

Now that we know the principle, it is much easier for us to carry out subsequent 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 the encryption algorithm used in Jasypt. In fact, it is in JCE of JDK Jar package. In essence, the algorithm provided by JDK is used. By default, the PBE algorithm pbewithmd5addes is used. It's interesting to see the naming of this algorithm. Let's see a sentence. PBE, WITH, MD5, AND, DES seem to have some stories. Continue.

PBE algorithm (Password Based Encryption) is a Password Based Encryption algorithm, which is characterized in that the password is mastered by the user and combined with multiple encryption of random numbers to ensure the security of data.

In essence, PBE algorithm does not really build new encryption and decryption algorithms, but packages our known algorithms. For example, the commonly used message digest algorithms MD5 and SHA, symmetric encryption algorithms DES and RC2, and the PBE algorithm is a reasonable combination of these algorithms, which also echoes the name of the previous algorithm.

Since the PBE algorithm uses our commonly used symmetric encryption algorithm, it will involve the problem of key. However, it has no concept of key, only password, and the key is calculated by the password encryption algorithm.

The password itself is not very long, so it can not be used to replace the key. Only the password can be easily decoded by exhaustive attack. At this time, we have to add some salt.

Salt is usually some random information, such as random number and timestamp. Salt is attached to the password, which increases the difficulty of decoding through algorithm calculation.

Fishy in the source code

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

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

public byte[] encrypt(byte[] message) {
    // Initialize the secret key factory according to the specified algorithm
    final SecretKeyFactory factory = SecretKeyFactory.getInstance(algorithm1);
    // Salt value generator, select only eight digits
    byte[] salt = saltGenerator.generateSalt(8);
    // 
    final PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, iterations);
    // Salt value and password generation secret key
    SecretKey key = factory.generateSecret(keySpec);

    // Build encryptor
    final Cipher cipherEncrypt = Cipher.getInstance(algorithm1);
    cipherEncrypt.init(Cipher.ENCRYPT_MODE, key);
    // Ciphertext header (salt value)
    byte[] params = cipherEncrypt.getParameters().getEncoded();

    // Call the underlying implementation encryption
    byte[] encryptedMessage = cipherEncrypt.doFinal(message);

    // Assemble the final ciphertext content and allocate memory (salt value + ciphertext)
    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 content of the same content is different after each encryption.

So how to correspond when decrypting?

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

During decryption, the params content will be disassembled according to the content of the ciphertext encryptedMessage, the salt value and password will be parsed, and the actual content will be decrypted by calling the underlying algorithm of JDK.

@Override
@SneakyThrows
public byte[] decrypt(byte[] encryptedMessage) {
    // Get ciphertext header content
    int paramsLength = Byte.toUnsignedInt(encryptedMessage[0]);
    // Get ciphertext content
    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);

    // Initialize secret key factory
    final SecretKeyFactory factory = SecretKeyFactory.getInstance(algorithm1);
    final PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray());
    SecretKey key = factory.generateSecret(keySpec);

    // Build header salt value password parameters
    AlgorithmParameters algorithmParameters = AlgorithmParameters.getInstance(algorithm1);
    algorithmParameters.init(params);

    // Build the encryptor and call the underlying algorithm
    final Cipher cipherDecrypt = Cipher.getInstance(algorithm1);
    cipherDecrypt.init(
            Cipher.DECRYPT_MODE,
            key,
            algorithmParameters
    );
    return cipherDecrypt.doFinal(message);
}

Topics: Java Programming Spring Boot Interview Programmer