From f0dca7914e80eed69d97e0b22244eaccded59d22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?brother-=E6=88=8E?= <767817253@qq.com> Date: Thu, 9 Nov 2023 13:43:03 +0800 Subject: [PATCH] support kmsv3 (#8) * support kmsv3 * tiny fix * fix #6 * tiny fix * version 1.0.3 * add log keyId * add key deletion protection * fix * add no-args constructor * fix up * fix up mvn dependencies bug * add exception infomation * add exception infomation * throw nacos exception * throw nacos exception * ca is not necessary * change log level * ca is not necessary --- .gitignore | 2 +- pom.xml | 35 +- .../client/aliyun/AliyunConfigFilter.java | 410 ++++++++++++++++-- .../nacos/client/aliyun/AliyunConst.java | 47 ++ .../nacos/client/aliyun/AsyncProcessor.java | 71 +++ .../client/aliyun/AliyunConfigFilterTest.java | 97 +++++ src/test/resources/aliyun-kms.properties | 19 + src/test/resources/ca.pem | 2 + src/test/resources/client_key.json | 4 + 9 files changed, 642 insertions(+), 45 deletions(-) create mode 100644 src/main/java/com/alibaba/nacos/client/aliyun/AsyncProcessor.java create mode 100644 src/test/java/com/alibaba/nacos/client/aliyun/AliyunConfigFilterTest.java create mode 100644 src/test/resources/aliyun-kms.properties create mode 100644 src/test/resources/ca.pem create mode 100644 src/test/resources/client_key.json diff --git a/.gitignore b/.gitignore index 70afd1b..0b144da 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,4 @@ hs_err_pid* .project .settings/ *.iml -target/ +target/ \ No newline at end of file diff --git a/pom.xml b/pom.xml index cc2c12f..598944e 100644 --- a/pom.xml +++ b/pom.xml @@ -17,7 +17,7 @@ 2018 com.alibaba.nacos nacos-client-mse-extension - 1.0.2-beta + 1.0.3 jar @@ -51,26 +51,39 @@ UTF-8 - 1.4.2-SNAPSHOT + 2.2.4 com.aliyun aliyun-java-sdk-core - 4.5.20 + 4.5.17 com.aliyun aliyun-java-sdk-kms - 2.14.0 + 2.16.3 + + + + com.aliyun.kms + kms-transfer-client + + + com.aliyun + aliyun-java-sdk-kms + + + 0.1.0 com.alibaba.nacos nacos-api ${nacos.version} + true @@ -79,6 +92,20 @@ 1.15 + + + org.junit.jupiter + junit-jupiter + RELEASE + test + + + com.alibaba.nacos + nacos-client + ${nacos.version} + test + + diff --git a/src/main/java/com/alibaba/nacos/client/aliyun/AliyunConfigFilter.java b/src/main/java/com/alibaba/nacos/client/aliyun/AliyunConfigFilter.java index c94639a..3e391ad 100644 --- a/src/main/java/com/alibaba/nacos/client/aliyun/AliyunConfigFilter.java +++ b/src/main/java/com/alibaba/nacos/client/aliyun/AliyunConfigFilter.java @@ -7,21 +7,37 @@ import com.alibaba.nacos.api.config.filter.IConfigResponse; import com.alibaba.nacos.api.exception.NacosException; import com.alibaba.nacos.api.utils.StringUtils; -import com.aliyuncs.DefaultAcsClient; +import com.aliyun.dkms.gcs.openapi.models.Config; +import com.aliyun.kms.KmsTransferAcsClient; +import com.aliyuncs.IAcsClient; import com.aliyuncs.auth.AlibabaCloudCredentialsProvider; import com.aliyuncs.auth.InstanceProfileCredentialsProvider; import com.aliyuncs.exceptions.ClientException; import com.aliyuncs.http.FormatType; +import com.aliyuncs.http.HttpClientConfig; import com.aliyuncs.http.MethodType; import com.aliyuncs.http.ProtocolType; -import com.aliyuncs.kms.model.v20160120.DecryptRequest; -import com.aliyuncs.kms.model.v20160120.EncryptRequest; +import com.aliyuncs.kms.model.v20160120.DescribeKeyRequest; +import com.aliyuncs.kms.model.v20160120.DescribeKeyResponse; import com.aliyuncs.kms.model.v20160120.GenerateDataKeyRequest; import com.aliyuncs.kms.model.v20160120.GenerateDataKeyResponse; +import com.aliyuncs.kms.model.v20160120.DecryptRequest; +import com.aliyuncs.kms.model.v20160120.EncryptRequest; +import com.aliyuncs.kms.model.v20160120.SetDeletionProtectionRequest; import com.aliyuncs.profile.DefaultProfile; import com.aliyuncs.profile.IClientProfile; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashSet; import java.util.Properties; +import java.util.Set; /** * the IConfigFilter of Aliyun. @@ -30,6 +46,8 @@ */ public class AliyunConfigFilter extends AbstractConfigFilter { + private static final Logger LOGGER = LoggerFactory.getLogger(AliyunConfigFilter.class); + private static final String GROUP = "group"; private static final String DATA_ID = "dataId"; @@ -46,6 +64,8 @@ public class AliyunConfigFilter extends AbstractConfigFilter { private static final String CIPHER_PREFIX = "cipher-"; + private static AliyunConst.KmsVersion kmsVersion; + public static final String CIPHER_KMS_AES_128_PREFIX = "cipher-kms-aes-128-"; public static final String CIPHER_KMS_AES_256_PREFIX = "cipher-kms-aes-256-"; @@ -54,39 +74,226 @@ public class AliyunConfigFilter extends AbstractConfigFilter { public static final String KMS_KEY_SPEC_AES_256 = "AES_256"; - private DefaultAcsClient kmsClient; + private IAcsClient kmsClient; private String keyId; + private final Set addedKeys = new HashSet(); + + private AsyncProcessor asyncProcessor; + + private Exception localInitException; + @Override public void init(Properties properties) { - keyId = properties.getProperty(KEY_ID); - String regionId = properties.getProperty(REGION_ID); - String ramRoleName = properties.getProperty(PropertyKeyConst.RAM_ROLE_NAME); - String accessKey = properties.getProperty(PropertyKeyConst.ACCESS_KEY); - String secretKey = properties.getProperty(PropertyKeyConst.SECRET_KEY); - String kmsEndpoint = properties.getProperty(AliyunConst.KMS_ENDPOINT); - - if(StringUtils.isEmpty(regionId)){ - regionId = System.getProperty(KMS_REGION_ID, System.getenv(KMS_REGION_ID)); + LOGGER.info("init ConfigFilter: {}, for more information, please check: {}", + this.getFilterName(), AliyunConst.MSE_ENCRYPTED_CONFIG_USAGE_DOCUMENT_URL); + // get kms version, default using kms v1 + String kv = properties.getProperty(AliyunConst.KMS_VERSION_KEY, + System.getProperty(AliyunConst.KMS_VERSION_KEY, System.getenv(AliyunConst.KMS_VERSION_KEY))); + if (StringUtils.isBlank(kv)) { + LOGGER.warn("kms version is not set, using kms v1 version."); + kmsVersion = AliyunConst.KmsVersion.Kmsv1; + } else { + kmsVersion = AliyunConst.KmsVersion.fromValue(kv); + if (kmsVersion == AliyunConst.KmsVersion.UNKNOWN_VERSION) { + LOGGER.warn("kms version is not supported, using kms v1 version."); + kmsVersion = AliyunConst.KmsVersion.Kmsv1; + } else { + LOGGER.info("using kms version {}.", kmsVersion.getValue()); + } + } + + //keyId corresponding to the id/alias of KMS's secret key, using mseServiceKeyId by default + keyId = properties.getProperty(KEY_ID, System.getProperty(KEY_ID, System.getenv(KEY_ID))); + if (StringUtils.isBlank(keyId)) { + if (kmsVersion == AliyunConst.KmsVersion.Kmsv1) { + keyId = AliyunConst.KMS_DEFAULT_KEY_ID_VALUE; + LOGGER.info("using default keyId {}.", keyId); + } else { + String errorMsg = "keyId is not set up yet, unable to encrypt the configuration."; + localInitException = new RuntimeException(errorMsg); + LOGGER.error(AliyunConst.formatHelpMessage(errorMsg), localInitException); + return; + } + } else { + LOGGER.info("using keyId {}.", keyId); + } + + try { + if (kmsVersion == AliyunConst.KmsVersion.Kmsv1) { + kmsClient = createKmsV1Client(properties); + } else if (kmsVersion == AliyunConst.KmsVersion.Kmsv3) { + kmsClient = createKmsV3Client(properties); + } + } catch (ClientException e) { + LOGGER.error(AliyunConst.formatHelpMessage("kms init failed."), e); + localInitException = e; + } catch (Exception e) { + LOGGER.error(AliyunConst.formatHelpMessage("create kms client failed."), e); + localInitException = e; } - - if (System.getProperties().containsKey(AliyunConst.KMS_ENDPOINT)) { - kmsEndpoint = System.getProperty(AliyunConst.KMS_ENDPOINT); + try { + asyncProcessor = new AsyncProcessor(); + } catch (Exception e) { + LOGGER.error("init async processor failed.", e); + } + } + + /** + * init kms v1 client, accessing the KMS service through a shared gateway. + * + * @date 2023/9/19 + * @description + * @param properties + * @return com.aliyuncs.IAcsClient + * @throws + */ + private IAcsClient createKmsV1Client(Properties properties) { + String regionId = properties.getProperty(REGION_ID, System.getProperty(REGION_ID, System.getenv(REGION_ID))); + String kmsRegionId = properties.getProperty(KMS_REGION_ID, System.getProperty(KMS_REGION_ID, System.getenv(KMS_REGION_ID))); + if (StringUtils.isBlank(regionId)) { + regionId = kmsRegionId; + } + LOGGER.info("using regionId {}.", regionId); + if (StringUtils.isBlank(kmsRegionId)) { + kmsRegionId = regionId; } + LOGGER.info("using kms regionId {}.", kmsRegionId); + + if (StringUtils.isBlank(kmsRegionId) && StringUtils.isBlank(regionId)) { + String errorMsg = "region is not set up yet"; + LOGGER.error(AliyunConst.formatHelpMessage(errorMsg)); + localInitException = new RuntimeException(errorMsg); + return null; + } + + String ramRoleName= properties.getProperty(PropertyKeyConst.RAM_ROLE_NAME, + System.getProperty(PropertyKeyConst.RAM_ROLE_NAME, System.getenv(PropertyKeyConst.RAM_ROLE_NAME))); + LOGGER.info("using ramRoleName {}.", ramRoleName); + + String accessKey = properties.getProperty(PropertyKeyConst.ACCESS_KEY, + System.getProperty(PropertyKeyConst.ACCESS_KEY, System.getenv(PropertyKeyConst.ACCESS_KEY))); + LOGGER.info("using accessKey {}.", accessKey); + + String secretKey = properties.getProperty(PropertyKeyConst.SECRET_KEY, + System.getProperty(PropertyKeyConst.SECRET_KEY, System.getenv(PropertyKeyConst.SECRET_KEY))); + + String kmsEndpoint = properties.getProperty(AliyunConst.KMS_ENDPOINT, + System.getProperty(AliyunConst.KMS_ENDPOINT, System.getenv(AliyunConst.KMS_ENDPOINT))); if (!StringUtils.isBlank(kmsEndpoint)) { DefaultProfile.addEndpoint(regionId, "kms", kmsEndpoint); } + LOGGER.info("using kmsEndpoint {}.", kmsEndpoint); + IClientProfile profile = null; + IAcsClient kmsClient = null; if (!StringUtils.isBlank(ramRoleName)) { profile = DefaultProfile.getProfile(regionId); AlibabaCloudCredentialsProvider alibabaCloudCredentialsProvider = new InstanceProfileCredentialsProvider( ramRoleName); - kmsClient = new DefaultAcsClient(profile, alibabaCloudCredentialsProvider); + kmsClient = new KmsTransferAcsClient(profile, alibabaCloudCredentialsProvider); + LOGGER.info("successfully create kms client by using RAM role."); } else { profile = DefaultProfile.getProfile(regionId, accessKey, secretKey); - kmsClient = new DefaultAcsClient(profile); + kmsClient = new KmsTransferAcsClient(profile); + LOGGER.info("successfully create kms client by using ak/sk."); } + return kmsClient; + } + + /** + * init kms v3 client, accessing the KMS service through the KMS instance gateway. + * + * @date 2023/9/19 + * @description + * @param properties + * @return + * @throws + */ + private IAcsClient createKmsV3Client(Properties properties) throws ClientException { + Config config = new Config(); + config.setProtocol("https"); + IClientProfile profile = null; + + String kmsClientKeyContent = properties.getProperty(AliyunConst.KMS_CLIENT_KEY_CONTENT_KEY, + System.getProperty(AliyunConst.KMS_CLIENT_KEY_CONTENT_KEY, System.getenv(AliyunConst.KMS_CLIENT_KEY_CONTENT_KEY))); + if (!StringUtils.isBlank(kmsClientKeyContent)) { + LOGGER.info("using {}: {}.", AliyunConst.KMS_CLIENT_KEY_CONTENT_KEY, kmsClientKeyContent); + config.setClientKeyContent(kmsClientKeyContent); + } else { + String errorMsg = null; + LOGGER.info("{} is empty, will read from file.", AliyunConst.KMS_CLIENT_KEY_CONTENT_KEY); + String kmsClientKeyFilePath = properties.getProperty(AliyunConst.KMS_CLIENT_KEY_FILE_PATH_KEY, + System.getProperty(AliyunConst.KMS_CLIENT_KEY_FILE_PATH_KEY, System.getenv(AliyunConst.KMS_CLIENT_KEY_FILE_PATH_KEY))); + if (!StringUtils.isBlank(kmsClientKeyFilePath)) { + String s = readFileToString(kmsClientKeyFilePath); + if (!StringUtils.isBlank(s)) { + LOGGER.info("using kmsClientKeyFilePath: {}.", kmsClientKeyFilePath); + config.setClientKeyFile(kmsClientKeyFilePath); + } else { + errorMsg = "both config from kmsClientKeyContent and kmsClientKeyFilePath is empty"; + } + } else { + errorMsg = "kmsClientKeyFilePath is empty"; + } + if (!StringUtils.isBlank(errorMsg)) { + localInitException = new RuntimeException(errorMsg); + return null; + } + } + + String kmsEndpoint = properties.getProperty(AliyunConst.KMS_ENDPOINT, + System.getProperty(AliyunConst.KMS_ENDPOINT, System.getenv(AliyunConst.KMS_ENDPOINT))); + if (StringUtils.isBlank(kmsEndpoint)) { + String errorMsg = String.format("%s is empty", AliyunConst.KMS_ENDPOINT); + localInitException = new RuntimeException(errorMsg); + return null; + } else { + LOGGER.info("using kmsEndpoint: {}.", kmsEndpoint); + config.setEndpoint(kmsEndpoint); + } + + String kmsPassword = properties.getProperty(AliyunConst.KMS_PASSWORD_KEY, + System.getProperty(AliyunConst.KMS_PASSWORD_KEY, System.getenv(AliyunConst.KMS_PASSWORD_KEY))); + if (StringUtils.isBlank(kmsPassword)) { + String errorMsg = String.format("%s is empty", AliyunConst.KMS_PASSWORD_KEY); + localInitException = new RuntimeException(errorMsg); + return null; + } else { + LOGGER.info("using kmsPassword prefix: {}.", kmsPassword.substring(kmsPassword.length() / 8)); + config.setPassword(kmsPassword); + } + + String kmsCaFileContent = properties.getProperty(AliyunConst.KMS_CA_FILE_CONTENT, + System.getProperty(AliyunConst.KMS_CA_FILE_CONTENT, System.getenv(AliyunConst.KMS_CA_FILE_CONTENT))); + if (!StringUtils.isBlank(kmsCaFileContent)) { + LOGGER.info("using {}: {}.", AliyunConst.KMS_CA_FILE_CONTENT, kmsCaFileContent); + config.setCa(kmsCaFileContent); + } else { + String errorMsg = null; + LOGGER.info("{} is empty, will read from file.", AliyunConst.KMS_CA_FILE_CONTENT); + String kmsCaFilePath = properties.getProperty(AliyunConst.KMS_CA_FILE_PATH_KEY, + System.getProperty(AliyunConst.KMS_CA_FILE_PATH_KEY, System.getenv(AliyunConst.KMS_CA_FILE_PATH_KEY))); + if (!StringUtils.isBlank(kmsCaFilePath)) { + config.setCaFilePath(kmsCaFilePath); + } else { + errorMsg = "kmsCaFilePath is empty"; + config.setCaFilePath(null); + } + if (!StringUtils.isBlank(errorMsg)) { + LOGGER.warn(AliyunConst.formatHelpMessage(errorMsg)); + profile = DefaultProfile.getProfile(config.getRegionId(), "ak", "sk", "sts"); + HttpClientConfig httpClientConfig = HttpClientConfig.getDefault(); + httpClientConfig.setIgnoreSSLCerts(true); + profile.setHttpClientConfig(httpClientConfig); + } + } + + if (profile == null) { + return new KmsTransferAcsClient(config); + } + return new KmsTransferAcsClient(profile, config); } @Override @@ -99,7 +306,7 @@ public void doFilter(IConfigRequest request, IConfigResponse response, IConfigFi dataId = (String) request.getParameter(DATA_ID); group = (String) request.getParameter(GROUP); if (dataId.startsWith(CIPHER_PREFIX)) { - if (request.getParameter(CONTENT) != null) { + if (!StringUtils.isBlank((String)request.getParameter(CONTENT))) { request.putParameter(CONTENT, encrypt(keyId, request)); } } @@ -107,39 +314,63 @@ public void doFilter(IConfigRequest request, IConfigResponse response, IConfigFi filterChain.doFilter(request, response); } if (response != null) { - dataId = (String) response.getParameter("dataId"); - group = (String) response.getParameter("group"); + dataId = (String) response.getParameter(DATA_ID); + group = (String) response.getParameter(GROUP); if (dataId.startsWith(CIPHER_PREFIX)) { - response.putParameter("content", decrypt(response)); + if (!StringUtils.isBlank((String)response.getParameter(CONTENT))) { + response.putParameter(CONTENT, decrypt(response)); + } } } } catch (ClientException e) { - e.printStackTrace(); - String message = String.format("KMS error, dataId: %s, groupId: %s", dataId, group); - throw new NacosException(NacosException.HTTP_CLIENT_ERROR_CODE, message, e); + String message = String.format("KMS message:[%s], error message:[%s], dataId: %s, groupId: %s", e.getMessage(), e.getErrMsg(), dataId, group); + throw new NacosException(NacosException.HTTP_CLIENT_ERROR_CODE, AliyunConst.formatHelpMessage(message), e); } catch (Exception e) { - NacosException ee = new NacosException(); - ee.setCauseThrowable(e); + StringBuilder stringBuilder = new StringBuilder(); + for (StackTraceElement ste : e.getStackTrace()) { + stringBuilder.append(ste.toString()).append("\n"); + } + NacosException ee = new NacosException(NacosException.INVALID_PARAM, AliyunConst.formatHelpMessage(stringBuilder.toString()), e); throw ee; } } private String decrypt(IConfigResponse response) throws Exception { - String dataId = (String) response.getParameter("dataId"); - String content = (String) response.getParameter("content"); + String dataId = (String) response.getParameter(DATA_ID); + String content = (String) response.getParameter(CONTENT); + String result; if (dataId.startsWith(CIPHER_KMS_AES_128_PREFIX) || dataId.startsWith(CIPHER_KMS_AES_256_PREFIX)) { - String encryptedDataKey = (String) response.getParameter("encryptedDataKey"); + String encryptedDataKey = (String) response.getParameter(ENCRYPTED_DATA_KEY); if (!StringUtils.isBlank(encryptedDataKey)) { String dataKey = decrypt(encryptedDataKey); - return AesUtils.decrypt((String) response.getParameter("content"), dataKey, "UTF-8"); + if (StringUtils.isBlank(dataKey)) { + throw new RuntimeException("failed to decrypt encryptedDataKey with empty value"); + } + result = AesUtils.decrypt((String) response.getParameter(CONTENT), dataKey, "UTF-8"); + if (StringUtils.isBlank(result)) { + throw new RuntimeException("failed to decrypt content with empty value"); + } + } else { + throw new RuntimeException("encrypted failed encryptedDataKey is empty"); } - return ""; } else { - return decrypt(content); + result = decrypt(content); + if (StringUtils.isBlank(result)) { + throw new RuntimeException("failed to decrypt content with empty value"); + } } + return result; } - private String decrypt(String content) throws ClientException { + private String decrypt(String content) throws Exception { + if (kmsClient == null) { + if (localInitException != null) { + throw localInitException; + } else { + throw new RuntimeException("kms client isn't initialized. " + + "For more information, please check: " + AliyunConst.MSE_ENCRYPTED_CONFIG_USAGE_DOCUMENT_URL); + } + } final DecryptRequest decReq = new DecryptRequest(); decReq.setSysProtocol(ProtocolType.HTTPS); decReq.setSysMethod(MethodType.POST); @@ -149,6 +380,20 @@ private String decrypt(String content) throws ClientException { } private String encrypt(String keyId, IConfigRequest configRequest) throws Exception { + if (kmsClient == null) { + if (localInitException != null) { + throw localInitException; + } else { + throw new RuntimeException("kms client isn't initialized. " + + "For more information, please check: " + AliyunConst.MSE_ENCRYPTED_CONFIG_USAGE_DOCUMENT_URL); + } + } + if (StringUtils.isBlank(keyId)) { + throw new RuntimeException("keyId is not set up yet, unable to encrypt the configuration. " + + "For more information, please check: " + AliyunConst.MSE_ENCRYPTED_CONFIG_USAGE_DOCUMENT_URL); + } + String result; + protectKeyId(keyId); String dataId = (String) configRequest.getParameter(DATA_ID); if (dataId.startsWith(CIPHER_KMS_AES_128_PREFIX) || dataId.startsWith(CIPHER_KMS_AES_256_PREFIX)) { @@ -159,15 +404,24 @@ private String encrypt(String keyId, IConfigRequest configRequest) throws Except keySpec = KMS_KEY_SPEC_AES_256; } GenerateDataKeyResponse generateDataKeyResponse = generateDataKey(keyId, keySpec); - configRequest.putParameter(ENCRYPTED_DATA_KEY, generateDataKeyResponse.getCiphertextBlob()); String dataKey = generateDataKeyResponse.getPlaintext(); - return AesUtils.encrypt((String) configRequest.getParameter(CONTENT), dataKey, "UTF-8"); + if (StringUtils.isBlank(dataKey.trim())) { + throw new RuntimeException("get generateDataKey failed with empty content. " + + "For more information, please check: " + AliyunConst.MSE_ENCRYPTED_CONFIG_USAGE_DOCUMENT_URL); + } + configRequest.putParameter(ENCRYPTED_DATA_KEY, generateDataKeyResponse.getCiphertextBlob()); + result = AesUtils.encrypt((String) configRequest.getParameter(CONTENT), dataKey, "UTF-8"); + } else { + result = encrypt(keyId, (String) configRequest.getParameter(CONTENT)); } - return encrypt(keyId, (String) configRequest.getParameter(CONTENT)); + if (StringUtils.isBlank(result)) { + throw new RuntimeException("encrypt failed with empty result."); + } + return result; } - private String encrypt(String keyId, String plainText) throws Exception { + public String encrypt(String keyId, String plainText) throws Exception { final EncryptRequest encReq = new EncryptRequest(); encReq.setProtocol(ProtocolType.HTTPS); encReq.setAcceptFormat(FormatType.JSON); @@ -177,7 +431,7 @@ private String encrypt(String keyId, String plainText) throws Exception { return kmsClient.getAcsResponse(encReq).getCiphertextBlob(); } - private GenerateDataKeyResponse generateDataKey(String keyId, String keySpec) throws ClientException { + public GenerateDataKeyResponse generateDataKey(String keyId, String keySpec) throws ClientException { GenerateDataKeyRequest generateDataKeyRequest = new GenerateDataKeyRequest(); generateDataKeyRequest.setAcceptFormat(FormatType.JSON); @@ -187,9 +441,85 @@ private GenerateDataKeyResponse generateDataKey(String keyId, String keySpec) th return kmsClient.getAcsResponse(generateDataKeyRequest); } + private void protectKeyId(String keyId) { + if (!addedKeys.contains(keyId)) { + synchronized (addedKeys) { + addedKeys.add(keyId); + asyncProcessor.addTack(new Runnable() { + @Override + public void run() { + try { + if (kmsClient == null) { + LOGGER.error("kms client hasn't initiated."); + return; + } + DescribeKeyRequest describeKeyRequest = new DescribeKeyRequest(); + describeKeyRequest.setKeyId(keyId); + try { + DescribeKeyResponse describeKeyResponse = kmsClient.getAcsResponse(describeKeyRequest); + if (describeKeyResponse.getKeyMetadata()!= null) { + String arn = describeKeyResponse.getKeyMetadata().getArn(); + LOGGER.info("set deletion protection for keyId[{}], arn[{}]", keyId, arn); + + SetDeletionProtectionRequest setDeletionProtectionRequest = new SetDeletionProtectionRequest(); + setDeletionProtectionRequest.setProtectedResourceArn(arn); + setDeletionProtectionRequest.setEnableDeletionProtection(true); + setDeletionProtectionRequest.setDeletionProtectionDescription("key is used by nacos-client"); + try { + kmsClient.getAcsResponse(setDeletionProtectionRequest); + } catch (ClientException e) { + LOGGER.error("set deletion protect failed, keyId: {}.", keyId); + throw e; + } + } else { + addedKeys.remove(keyId); + LOGGER.warn("keyId meta is null, cannot set key protection"); + } + } catch (ClientException e) { + LOGGER.error("describe key failed, keyId: {}.", keyId); + throw e; + } + } catch (Exception e) { + addedKeys.remove(keyId); + LOGGER.error("execute async task failed", e); + } + + } + }); + } + } + } + + private static String readFileToString(String filePath) { + File file = getFileByPath(filePath); + if (file == null || !file.exists()) { + return null; + } + try { + Path path = Paths.get(file.getAbsolutePath()); + byte[] fileContent = Files.readAllBytes(path); + return new String(fileContent, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static File getFileByPath(String filePath) { + File file = new File(filePath); + if (!file.exists()) { + String path = AliyunConfigFilter.class.getClassLoader().getResource("").getPath(); + if (!(file = new File(path + filePath)).exists()) { + path = Paths.get(filePath).toAbsolutePath().toString(); + if (!(file = new File(path)).exists()) { + return null; + } + } + } + return file; + } @Override public int getOrder() { - return 0; + return 1; } @Override diff --git a/src/main/java/com/alibaba/nacos/client/aliyun/AliyunConst.java b/src/main/java/com/alibaba/nacos/client/aliyun/AliyunConst.java index 6379031..694d8ca 100644 --- a/src/main/java/com/alibaba/nacos/client/aliyun/AliyunConst.java +++ b/src/main/java/com/alibaba/nacos/client/aliyun/AliyunConst.java @@ -7,4 +7,51 @@ */ public class AliyunConst { public static final String KMS_ENDPOINT = "kmsEndpoint"; + + public static final String KMS_VERSION_KEY = "kmsVersion"; + + public static final String KMS_DEFAULT_KEY_ID_VALUE = "alias/acs/mse"; + + public static final String KMS_CLIENT_KEY_FILE_PATH_KEY = "kmsClientKeyFilePath"; + + public static final String KMS_CLIENT_KEY_CONTENT_KEY = "kmsClientKeyContent"; + + public static final String KMS_PASSWORD_KEY = "kmsPasswordKey"; + + public static final String KMS_CA_FILE_PATH_KEY = "kmsCaFilePath"; + + public static final String KMS_CA_FILE_CONTENT = "kmsCaFileContent"; + + public static final String MSE_ENCRYPTED_CONFIG_USAGE_DOCUMENT_URL = "https://help.aliyun.com/zh/mse/user-guide/create-and-use-encrypted-configurations?spm=a2c4g.11186623.0.0.55587becdOW3jf"; + + public static String formatHelpMessage(String errorMessage) { + return String.format("%s, for more information, please check: %s", + errorMessage, AliyunConst.MSE_ENCRYPTED_CONFIG_USAGE_DOCUMENT_URL); + } + + public enum KmsVersion { + Kmsv1("v1.0"), + Kmsv3("v3.0"), + UNKNOWN_VERSION("unknown version"); + + private String value; + + KmsVersion(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static KmsVersion fromValue(String value) { + for (KmsVersion version : values()) { + if (version.getValue().equals(value)) { + return version; + } + } + return UNKNOWN_VERSION; + } + + } } diff --git a/src/main/java/com/alibaba/nacos/client/aliyun/AsyncProcessor.java b/src/main/java/com/alibaba/nacos/client/aliyun/AsyncProcessor.java new file mode 100644 index 0000000..d428b60 --- /dev/null +++ b/src/main/java/com/alibaba/nacos/client/aliyun/AsyncProcessor.java @@ -0,0 +1,71 @@ +package com.alibaba.nacos.client.aliyun; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.atomic.AtomicBoolean; + +public class AsyncProcessor { + + private static final int QUEUE_INITIAL_CAPACITY = 8; + + private static final String DEFAULT_PROCESSOR_NAME = "asyncProcessor"; + + private static final Logger LOGGER = LoggerFactory.getLogger(AsyncProcessor.class); + + private final BlockingQueue queue; + + private final AtomicBoolean closed; + + private final String name; + + public AsyncProcessor() { + this(QUEUE_INITIAL_CAPACITY, DEFAULT_PROCESSOR_NAME); + } + + public AsyncProcessor(int queueSize, String name) { + this.queue = new ArrayBlockingQueue(queueSize); + this.closed = new AtomicBoolean(false); + this.name = name; + (new InnerWorker(name)).start(); + } + + public void addTack(Runnable task) { + try { + queue.put(task); + } catch (InterruptedException e) { + LOGGER.error(e.toString(), e); + } + } + + public void shutdown() { + queue.clear(); + closed.compareAndSet(false, true); + } + + public String getName() { + return name; + } + + private class InnerWorker extends Thread { + InnerWorker(String name) { + super(name); + } + @Override + public void run() { + while (!closed.get()) { + try { + Runnable task = queue.take(); + long begin = System.currentTimeMillis(); + task.run(); + long duration = System.currentTimeMillis(); + LOGGER.info("runner[{}] executed task {} cost {} ms", getName(), task, duration - begin); + } catch (InterruptedException e) { + LOGGER.error(e.toString(), e); + } + } + } + } +} diff --git a/src/test/java/com/alibaba/nacos/client/aliyun/AliyunConfigFilterTest.java b/src/test/java/com/alibaba/nacos/client/aliyun/AliyunConfigFilterTest.java new file mode 100644 index 0000000..7f96521 --- /dev/null +++ b/src/test/java/com/alibaba/nacos/client/aliyun/AliyunConfigFilterTest.java @@ -0,0 +1,97 @@ +package com.alibaba.nacos.client.aliyun; + +import com.alibaba.nacos.api.exception.NacosException; +import com.alibaba.nacos.api.utils.StringUtils; +import com.alibaba.nacos.client.config.filter.impl.ConfigFilterChainManager; +import com.alibaba.nacos.client.config.filter.impl.ConfigRequest; +import com.alibaba.nacos.client.config.filter.impl.ConfigResponse; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +public class AliyunConfigFilterTest { + private static final String ENCRYPTED_DATA_KEY = "encryptedDataKey"; + public static Properties properties; + public static final List dataIdList = new ArrayList(){{ + add("cipher-crypt"); + add("cipher-kms-aes-256-crypt"); + add("cipher-kms-aes-128-crypt"); + }}; + + public static final String content = "crypt"; + + public static final String group = "default"; + + @BeforeEach + public void preset() { + try { + properties = new Properties(); + properties.load(this.getClass().getResourceAsStream("/aliyun-kms.properties")); + } catch (Exception e) { + e.printStackTrace(); + } + + } + + @Test + public void testAliyunConfigFilterWithKmsV1() { + properties.setProperty(AliyunConst.KMS_VERSION_KEY, AliyunConst.KmsVersion.Kmsv1.getValue()); + //ignore kmsEndpoint + properties.setProperty("kmsEndpoint", ""); + properties.setProperty("regionId", "cn-beijing"); + properties.setProperty("kms_region_id", "cn-beijing"); + properties.setProperty("accessKey", "LTAxxxx1E6"); + properties.setProperty("secretKey", "kr6JxxxsD6"); + properties.setProperty("keyId", "alias/acs/mse"); + executeConfigFilter(); + } + + // must be running in vpc +// @Test +// public void testAliyunConfigFilterWithKmsV3() { +// properties.setProperty(AliyunConst.KMS_VERSION_KEY, AliyunConst.KmsVersion.Kmsv3.getValue()); +// properties.setProperty("keyId", "alias/chasu"); +// properties.setProperty("kmsEndpoint", "kst-bjxxxxxxxxxc.cryptoservice.kms.aliyuncs.com"); +// properties.setProperty("kmsClientKeyFilePath", "/client_key.json"); +// properties.setProperty("kmsPasswordKey", "19axxx213"); +// properties.setProperty("kmsCaFilePath", "/ca.pem"); +// executeConfigFilter(); +// } + + private void executeConfigFilter() { + for (String dataId : dataIdList) { + ConfigFilterChainManager configFilterChainManager = new ConfigFilterChainManager(properties); + AliyunConfigFilter aliyunConfigFilter = new AliyunConfigFilter(); + configFilterChainManager.addFilter(aliyunConfigFilter); + + ConfigRequest configRequest = new ConfigRequest(); + configRequest.setGroup(group); + configRequest.setDataId(dataId); + configRequest.setContent(content); + String encryptedContent = null; + try { + configFilterChainManager.doFilter(configRequest, null); + encryptedContent = configRequest.getContent(); + Assertions.assertFalse(StringUtils.isBlank(encryptedContent)); + } catch (NacosException e) { + e.printStackTrace(); + } + + ConfigResponse configResponse = new ConfigResponse(); + configResponse.setGroup(group); + configResponse.setDataId(dataId); + configResponse.setEncryptedDataKey((String) configRequest.getParameter(ENCRYPTED_DATA_KEY)); + configResponse.setContent(encryptedContent); + try { + configFilterChainManager.doFilter(null, configResponse); + Assertions.assertEquals(content, configResponse.getContent()); + } catch (NacosException e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/src/test/resources/aliyun-kms.properties b/src/test/resources/aliyun-kms.properties new file mode 100644 index 0000000..62f4a1a --- /dev/null +++ b/src/test/resources/aliyun-kms.properties @@ -0,0 +1,19 @@ +# v1 configuration +regionId=cn-beijing +kms_region_id=cn-beijing +accessKey=LTAxxx1E6 +secretKey=kr6xxxsD6 +kmsVersion=v1.0 +keyId=alias/kk + +# v3 configuration +# regionId=cn-beijing +# kms_region_id=cn-beijing +# accessKey=LTAxxx1E6 +# secretKey=kr6xxxsD6 +# keyId=alias/kk +# kmsVersion=v3.0 +kmsEndpoint=kst-xxxygc.cryptoservice.kms.aliyuncs.com +kmsClientKeyFilePath=client_key.json +kmsPasswordKey=19axxx213 +kmsCaFilePath=ca.pem diff --git a/src/test/resources/ca.pem b/src/test/resources/ca.pem new file mode 100644 index 0000000..c412709 --- /dev/null +++ b/src/test/resources/ca.pem @@ -0,0 +1,2 @@ +-----BEGIN CERTIFICATE----- +-----END CERTIFICATE----- diff --git a/src/test/resources/client_key.json b/src/test/resources/client_key.json new file mode 100644 index 0000000..6c37c9c --- /dev/null +++ b/src/test/resources/client_key.json @@ -0,0 +1,4 @@ +{ + "KeyId": "KAAxxxe74", + "PrivateKeyData": "MIIxxxA==" +} \ No newline at end of file