Jasypt jasypt-spring-boot

使用 Jasypt 自訂偵測及解密方法

黃紹溥 Kim Huang 2022/08/29 12:59:42
6547

  使用 Jasypt-spring-boot 自訂 解密器(Custom Encryptor) 之後,不論是預設的 PBE(Password-based Encryption),

或是 v2.1.1 版所支援的 非對稱式加密(Asymmetric Encryption),都會轉導至自訂的解密器進行處理,

以下教學如何讓自訂的解密器只在使用其中一種加密方式時生效。

1. Gradle 設定

plugins {
	id 'org.springframework.boot' version '2.7.1'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id 'java'
}

group = 'com.thinkpower'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter'
	implementation 'com.github.ulisesbocchio:jasypt-spring-boot-starter:2.1.2'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
	useJUnitPlatform()
}

 

2. application.properties 設定

在此範例中,我先將兩種演算法需要使用到的項目先準備好 (加密的方式可以參考這裡)。

# 使用 PBE 時的設定
jasypt.encryptor.password=123456

# 使用 PBE 所加密的字串
password=ENC(pm+1TlPBLU4R/USxTnC8OQ==)


# 使用 Asymmetric Encryption 時的設定
jasypt.encryptor.privateKeyFormat=PEM
jasypt.encryptor.privateKeyString=\
-----BEGIN PRIVATE KEY-----\
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCtB/IYK8E52CYMZTpyIY9U0HqMewyKnRvSo6s+9VNIn/HSh9+MoBGiADa2MaPKvetS3CD3CgwGq/+L\
IQ1HQYGchRrSORizOcIp7KBx+Wc1riatV/tcpcuFLC1j6QJ7d2I+T7RA98Sx8X39orqlYFQVysTw/aTawX/yajx0UlTW3rNAY+ykeQ0CBHowtTxKM9nGcxLoQbvbYx1i\
G9JgAqye7TYejOpviOH+BpD8To2S8zcOSojIhixEfayay0gURv0IKJN2LP86wkpAuAbL+mohUq1qLeWdTEBrIRXjlnrWs1M66w0l/6JwaFnGOqEB6haMzE4JWZULYYpr\
2yKyoGCRAgMBAAECggEAQxURhs1v3D0wgx27ywO3zeoFmPEbq6G9Z6yMd5wk7cMUvcpvoNVuAKCUlY4pMjDvSvCM1znN78g/CnGF9FoxJb106Iu6R8HcxOQ4T/ehS+54\
kDvL999PSBIYhuOPUs62B/Jer9FfMJ2veuXb9sGh19EFCWlMwILEV/dX+MDyo1qQaNzbzyyyaXP8XDBRDsvPL6fPxL4r6YHywfcPdBfTc71/cEPksG8ts6um8uAVYbLI\
DYcsWopjVZY/nUwsz49xBCyRcyPnlEUJedyF8HANfVEO2zlSyRshn/F+rrjD6aKBV/yVWfTEyTSxZrBPl4I4Tv89EG5CwuuGaSagxfQpAQKBgQDXEe7FqXSaGk9xzuPa\
zXy8okCX5pT6545EmqTP7/JtkMSBHh/xw8GPp+JfrEJEAJJl/ISbdsOAbU+9KAXuPmkicFKbodBtBa46wprGBQ8XkR4JQoBFj1SJf7Gj9ozmDycozO2Oy8a1QXKhHUPk\
bPQ0+w3efwoYdfE67ZodpFNhswKBgQDN9eaYrEL7YyD7951WiK0joq0BVBLK3rwO5+4g9IEEQjhP8jSo1DP+zS495t5ruuuuPsIeodA79jI8Ty+lpYqqCGJTE6muqLMJ\
Diy7KlMpe0NZjXrdSh6edywSz3YMX1eAP5U31pLk0itMDTf2idGcZfrtxTLrpRffumowdJ5qqwKBgF+XZ+JRHDN2aEM0atAQr1WEZGNfqG4Qx4o0lfaaNs1+H+knw5kI\
ohrAyvwtK1LgUjGkWChlVCXb8CoqBODMupwFAqKL/IDImpUhc/t5uiiGZqxE85B3UWK/7+vppNyIdaZL13a1mf9sNI/p2whHaQ+3WoW/P3R5z5uaifqM1EbDAoGAN584\
JnUnJcLwrnuBx1PkBmKxfFFbPeSHPzNNsSK3ERJdKOINbKbaX+7DlT4bRVbWvVj/jcw/c2Ia0QTFpmOdnivjefIuehffOgvU8rsMeIBsgOvfiZGx0TP3+CCFDfRVqjIB\
t3HAfAFyZfiP64nuzOERslL2XINafjZW5T0pZz8CgYAJ3UbEMbKdvIuK+uTl54R1Vt6FO9T5bgtHR4luPKoBv1ttvSC6BlalgxA0Ts/AQ9tCsUK2JxisUcVgMjxBVvG0\
lfq/EHpL0Wmn59SHvNwtHU2qx3Ne6M0nQtneCCfR78OcnqQ7+L+3YCMqYGJHNFSard+dewfKoPnWw0WyGFEWCg==\
-----END PRIVATE KEY-----

# 使用 Asymmetric Encryption 所加密的字串
#password=ENC(Jg2DISMYfFQbQKTCK4xLjXhtDc+cF8oU6Ha7ohU6Tal06942tr6psxmGuCnrWDh32YhGIB2qwI0KVtkR2RyhUuEHFycit673gI7ioXiLkLcH4TFJN3OV7es02O8GZWy+Thdnv0GMdIfZLp2ywi5lkCD0VEqhLX7wKxXUAwRrMZPlkhn/fE/eQP+mx0bw/mXYOleaxcrqm85c3rPLT8j3C+rpLCr1EWKXnPMCmU/bU9FeXCOBkpTGow/sWZgKKXdqb50yCdCXIAr3KiFaQbSI/EkAKJKHQlf7R2jyfcjrBUOhJAdmU2SU0ijzhinRUpRA3dtRtviYpPqbfigh6qHN0g==)

 

3. 實作 EncryptablePropertyResolver 介面,定義如何 偵測(detecting) 與 解密(decrypting)

import com.ulisesbocchio.jasyptspringboot.EncryptablePropertyResolver;

public class CustomEncryptablePropertyResolver implements EncryptablePropertyResolver {

	@Override
	public String resolvePropertyValue(String value) {
		// value 包含識別字 "ENC", 如: "ENC(pm+1TlPBLU4R/USxTnC8OQ==)
		// TODO 將 value 解密之後再回傳
		return value;
	}

}

我們的目標是希望在 PBE 模式下,使用預設的解密方式,但在 Asymmetric Encryption 模式下使用自訂的演算法,所以需要保留預設的加密器 (StringEncryptor),並用它來實現 resolvePropertValue 方法。

import org.jasypt.encryption.StringEncryptor;
import com.ulisesbocchio.jasyptspringboot.EncryptablePropertyResolver;

public class CustomEncryptablePropertyResolver implements EncryptablePropertyResolver {

	private final StringEncryptor stringEncryptor;

	public CustomEncryptablePropertyResolver(StringEncryptor defaultStringEncryptor) {
		this.stringEncryptor = defaultStringEncryptor;
	}

	@Override
	public String resolvePropertyValue(String value) {
		if (value != null && value.startsWith("ENC(")) {
			// 先拆解出識別字中被加密的字段
			String encrypted = value.substring(value.indexOf("ENC(") + 4, value.lastIndexOf(")"));
			// 使用預設加密器進行解密
			String decrypted = this.stringEncryptor.decrypt(encrypted);
			return decrypted;
		}
		return value;
	}

}

 

4. 註冊客製的 CustomEncryptablePropertyResolver 類別到 Spring Context

※ 注意:一旦註冊了 "encryptablePropertyResolver" 這個名稱的 Bean,則不論 PBE 或 非對稱式,一律都交由自訂的 Bean 處理加解密

import com.thinkpower.jasypt.component.CustomEncryptablePropertyResolver;
import com.ulisesbocchio.jasyptspringboot.EncryptablePropertyResolver;
import com.ulisesbocchio.jasyptspringboot.encryptor.DefaultLazyEncryptor;

@Configuration
public class JasyptConfig {

	@Autowired
	private DefaultLazyEncryptor defaultStringEncryptor;

	@Bean(name="encryptablePropertyResolver")
    public EncryptablePropertyResolver encryptablePropertyResolver() {
		// 建構時,傳入預設的 StringEncryptor
        return new CustomEncryptablePropertyResolver(defaultStringEncryptor);
    }

}

因此我們需要讀取 properties 檔,來判斷使用自訂解密演算法的時機,可以透過 JasyptEncryptorConfigurationProperties 類別來取得 jasypt-spring-boot 相關的設定值。

import com.thinkpower.jasypt.component.CustomEncryptablePropertyResolver;
import com.ulisesbocchio.jasyptspringboot.EncryptablePropertyResolver;
import com.ulisesbocchio.jasyptspringboot.encryptor.DefaultLazyEncryptor;
import com.ulisesbocchio.jasyptspringboot.properties.JasyptEncryptorConfigurationProperties;
import com.ulisesbocchio.jasyptspringboot.util.Singleton;

@Configuration
public class JasyptConfig {

	@Autowired
	private DefaultLazyEncryptor defaultStringEncryptor;

	// 關於 jasypt 的設定值 (from application-*.properties)
	@Autowired
	private Singleton<JasyptEncryptorConfigurationProperties> configPropsSingleton;

	@Bean(name="encryptablePropertyResolver")
    public EncryptablePropertyResolver encryptablePropertyResolver() {
		// 建構時,傳入預設的 StringEncryptor
        return new CustomEncryptablePropertyResolver(defaultStringEncryptor, configPropsSingleton);
    }

}

 

5. 修改 CustomEncryptablePropertyResolver 類別

jasypt-spring-boot 預設會先判斷 properties 中是否有 "jasypt.encryptor.password" 項目,若有,則一律使用 PBE 作為加解密演算法;若沒有,才會再判斷是否有 "jasypt.encryptor.private-key-string" 或 "jasypt.encryptor.private-key-location" 項目,若有,才會使用 Asymmetric Encryption 演算法。

我們參考預設的邏輯,並實作自定義的 加密(encrypt) 與 解密(decrypt) 方法:

import org.jasypt.encryption.StringEncryptor;
import org.springframework.util.ObjectUtils;

import com.ulisesbocchio.jasyptspringboot.EncryptablePropertyResolver;
import com.ulisesbocchio.jasyptspringboot.properties.JasyptEncryptorConfigurationProperties;
import com.ulisesbocchio.jasyptspringboot.util.Singleton;

public class CustomEncryptablePropertyResolver implements EncryptablePropertyResolver {

	private final StringEncryptor stringEncryptor;

	public CustomEncryptablePropertyResolver(StringEncryptor defaultStringEncryptor, //
			Singleton<JasyptEncryptorConfigurationProperties> configProps) {
		StringEncryptor encryptor = null;
    	try {
    		// 取得 properties 的設定值
    		String password = configProps.get().getPassword();
    		String privateKeyLocation = configProps.get().getPrivateKeyLocation();
			String privateKeyString = configProps.get().getPrivateKeyString();

			// 有設定 'jasypt.encryptor.password' 就走 PBE
			if (!ObjectUtils.isEmpty(password)) {
				System.out.println("Password-based Encryption Configuration detected!");
				encryptor = defaultStringEncryptor;

    		// 有設定 "jasypt.encryptor.private-key-string" 或 "jasypt.encryptor.private-key-location" 就走非對稱式
			} else if (!ObjectUtils.isEmpty(privateKeyString) || !ObjectUtils.isEmpty(privateKeyLocation)) {
				System.out.println("Asymmetric Encryption Configuration detected!");
				encryptor = new StringEncryptor() {
					@Override
					public String encrypt(String message) {
						// 主要目標是解密, 所以加密用預設的演算法即可
						return defaultStringEncryptor.encrypt(message);
					}
					
					@Override
					public String decrypt(String encryptedMessage) {
						// 使用預設演算法解密後, 故意在字串後面加上 "_TPI_FOREVER" 字樣,以利測試
						String decryptedMessage = defaultStringEncryptor.decrypt(encryptedMessage);
						decryptedMessage += "_TPI_FOREVER";
						return decryptedMessage;
					}
				};
			} else {
				throw new Exception("Either 'jasypt.encryptor.password' or"
					+ " one of ['jasypt.encryptor.private-key-string', 'jasypt.encryptor.private-key-location']"
					+ " must be provided for Password-based or Asymmetric encryption");
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
    	
    	this.stringEncryptor = encryptor;
	}

	@Override
	public String resolvePropertyValue(String value) {
		if (value != null && value.startsWith("ENC(")) {
			// 先拆解出識別字中被加密的字段
			String encrypted = value.substring(value.indexOf("ENC(") + 4, value.lastIndexOf(")"));
			// 使用預設加密器進行解密
			String decrypted = this.stringEncryptor.decrypt(encrypted);
			return decrypted;
		}
		return value;
	}

}

 

6. 建立測試類別

透過 @Value 取得解密後的值

import javax.annotation.PostConstruct;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Service
public class UserService {

	@Value("${password:}")
	private String password;

	@PostConstruct
	public void init() {
		System.out.println("Decrypted password: " + password);
	}

}

專案結構如下:

測試專案的結構

7. 驗證:分別透過 PBE 與 Asymmetric Encryption 取得 properties value 的結果

執行 com.thinkpower.jasypt.JasyptApplication.java

7-1. PBE 解密結果

可以看到解密出來的文字為 "abc",沒有加上 "TPI_FOREVER",表示不是使用自訂的演算法進行解密。

7-2. Asymmetric Encryption 解密結果

先修改 application.properties,讓非對稱式解密的設定生效:

再執行 JasyptApplication.java

可以看到解密出來的文字為 "abc_TPI_FOREVER",表示是使用自訂的演算法進行解密。

8. 結論

透過實作 jasypt-spring-boot 的 EncryptablePropertyResolver 介面,可以讓我們自訂 偵測(Detecting) 與 解密(Decrypting) 的演算法,並在註冊 Spring Bean 的時候傳入預設的解密器 (StringEncryptor) 與 Jasypt 設定值,可以讓我們自行決定何時採用自訂的演算法。

 

參考資料

https://github.com/ulisesbocchio/jasypt-spring-boot

https://www.baeldung.com/spring-boot-jasypt

黃紹溥 Kim Huang