From d3cfc6684a05b2f4c73c3be71fcaf740af812084 Mon Sep 17 00:00:00 2001 From: Jiahj Date: Thu, 25 Dec 2025 18:09:48 +0800 Subject: [PATCH 1/2] feat: add ExternalRsaProvider to support HSM/KMS-based RSA operations --- .../client/utils/ExternalRsaProvider.java | 79 +++++++++++++ .../com/safeheron/client/utils/RsaUtil.java | 14 +++ .../demo/api/transaction/TransactionTest.java | 111 ++++++++++++++++++ 3 files changed, 204 insertions(+) create mode 100644 src/main/java/com/safeheron/client/utils/ExternalRsaProvider.java diff --git a/src/main/java/com/safeheron/client/utils/ExternalRsaProvider.java b/src/main/java/com/safeheron/client/utils/ExternalRsaProvider.java new file mode 100644 index 0000000..df6f4ae --- /dev/null +++ b/src/main/java/com/safeheron/client/utils/ExternalRsaProvider.java @@ -0,0 +1,79 @@ +package com.safeheron.client.utils; + +import com.safeheron.client.config.RSATypeEnum; + +/** + * External RSA provider interface for delegating RSA cryptographic operations + * to external key management or signing systems. + * + *

This interface is designed to support scenarios where RSA private keys + * are managed by secure infrastructures such as HSM, KMS, or other + * centralized key management services. In such environments, private key + * material must never be exported or exposed to the application layer.

+ * + *

By introducing this abstraction, the SDK can delegate RSA signing and + * decryption operations to external systems via a {@code keyId}, instead of + * requiring the complete private key to be configured locally.

+ * + *

Typical use cases include:

+ * + * + *

Implementations of this interface are responsible for:

+ * + * + *

The SDK itself does not hold, persist, or process any RSA private key + * material when this provider is used.

+ * + * @author Jiahj + */ +public interface ExternalRsaProvider { + /** + * Sign the given content using RSA (PKCS#1 v1.5) with a key managed by an external system + * such as HSM or KMS. + * + *

The actual private key material must never be exposed to the SDK. + * Implementations should perform the signing operation by delegating to + * the external key management or signing service identified by {@code keyId}.

+ * + * @param content the plain text content to be signed + * @param keyId the identifier of the RSA key in the external signing system + * @return the Base64-encoded signature result + */ + String sign(String content, String keyId); + + /** + * Sign the given content using RSA-PSS with a key managed by an external system + * such as HSM or KMS. + * + *

This method is intended for scenarios requiring stronger cryptographic + * security guarantees compared to PKCS#1 v1.5.

+ * + * @param content the plain text content to be signed + * @param keyId the identifier of the RSA key in the external signing system + * @return the Base64-encoded RSA-PSS signature result + */ + String signPSS(String content, String keyId); + + /** + * Decrypt the given encrypted content using an RSA private key managed by + * an external system such as HSM or KMS. + * + *

The decryption operation must be performed by the external key management + * system, and the private key material must not be exposed to the SDK.

+ * + * @param content the encrypted content, typically Base64-encoded + * @param keyId the identifier of the RSA key in the external key management system + * @param rsaType the RSA algorithm type used for decryption + * @return the decrypted raw byte array + */ + byte[] decrypt(String content, String keyId, RSATypeEnum rsaType); + +} diff --git a/src/main/java/com/safeheron/client/utils/RsaUtil.java b/src/main/java/com/safeheron/client/utils/RsaUtil.java index 6e23c63..ae72715 100644 --- a/src/main/java/com/safeheron/client/utils/RsaUtil.java +++ b/src/main/java/com/safeheron/client/utils/RsaUtil.java @@ -26,11 +26,16 @@ public class RsaUtil { public static final String SIGN_ALGORITHMS_SHA256RSA_PSS = "SHA256withRSA/PSS"; private static final int MAX_ENCRYPT_BLOCK = 501; private static final int MAX_DECRYPT_BLOCK = 512; + private static ExternalRsaProvider externalRsaProvider; static { Security.addProvider(new BouncyCastleProvider()); } + public static void setExtProvider(ExternalRsaProvider lExternalRsaProvider) { + externalRsaProvider = lExternalRsaProvider; + } + public static String encrypt(byte[] plainText, String publicKey, RSATypeEnum RSAType) throws Exception { ByteArrayOutputStream out = null; try { @@ -68,6 +73,9 @@ public static String encrypt(byte[] plainText, String publicKey, RSATypeEnum RSA } public static byte[] decrypt(String content, String privateKey, RSATypeEnum RSAType) throws Exception { + if (externalRsaProvider != null) { + return externalRsaProvider.decrypt(content, privateKey, RSAType); + } ByteArrayOutputStream out = null; try { PrivateKey priKey = getPrivateKey(SIGN_TYPE_RSA, privateKey); @@ -106,6 +114,9 @@ public static byte[] decrypt(String content, String privateKey, RSATypeEnum RSAT } public static String sign(String content, String privateKey) throws Exception { + if (externalRsaProvider != null) { + return externalRsaProvider.sign(content, privateKey); + } PrivateKey priKey = getPrivateKey(SIGN_TYPE_RSA, privateKey); Signature privateSignature = Signature.getInstance(SIGN_ALGORITHMS_SHA256RSA); privateSignature.initSign(priKey); @@ -115,6 +126,9 @@ public static String sign(String content, String privateKey) throws Exception { } public static String signPSS(String content, String privateKey) throws Exception { + if (externalRsaProvider != null) { + return externalRsaProvider.signPSS(content, privateKey); + } PrivateKey priKey = getPrivateKey(SIGN_TYPE_RSA, privateKey); Signature privateSignature = Signature.getInstance(SIGN_ALGORITHMS_SHA256RSA_PSS); privateSignature.initSign(priKey); diff --git a/src/test/java/com/safeheron/demo/api/transaction/TransactionTest.java b/src/test/java/com/safeheron/demo/api/transaction/TransactionTest.java index 48cc509..6e693b3 100644 --- a/src/test/java/com/safeheron/demo/api/transaction/TransactionTest.java +++ b/src/test/java/com/safeheron/demo/api/transaction/TransactionTest.java @@ -1,9 +1,12 @@ package com.safeheron.demo.api.transaction; import com.safeheron.client.api.TransactionApiService; +import com.safeheron.client.config.RSATypeEnum; import com.safeheron.client.config.SafeheronConfig; import com.safeheron.client.request.CreateTransactionRequest; import com.safeheron.client.response.TxKeyResult; +import com.safeheron.client.utils.ExternalRsaProvider; +import com.safeheron.client.utils.RsaUtil; import com.safeheron.client.utils.ServiceCreator; import com.safeheron.client.utils.ServiceExecutor; import lombok.extern.slf4j.Slf4j; @@ -15,6 +18,12 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.Signature; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; import java.util.Map; import java.util.UUID; @@ -60,4 +69,106 @@ public void testSendTransaction(){ System.out.println(String.format("transaction has been created, txKey: %s", createTransactionResponse.getTxKey())); } + /** + * Test case for sending a transaction using an ExternalRsaProvider. + * + *

This test demonstrates how to integrate a custom {@link ExternalRsaProvider} + * to perform RSA signing through an external system (e.g. HSM / KMS), + * instead of configuring the complete RSA private key in the SDK.

+ * + *

For demonstration purposes, this test uses a mock implementation + * of {@code ExternalRsaProvider} that performs local RSA signing. + * In real production scenarios, the signing logic should be delegated + * to a secure key management or signing service, identified by {@code keyId}.

+ */ + @Test + public void testSendTransactionWithExternalRsaProvider() { + + /** + * Mock implementation of ExternalRsaProvider. + * + *

This implementation simulates an external signing system by + * performing RSA signing locally using a private key loaded from + * configuration. It is intended for testing and demonstration only.

+ */ + ExternalRsaProvider mockRsaProvider = new ExternalRsaProvider() { + + /** + * Sign the given content using RSA (SHA256withRSA). + * + *

The {@code keyId} parameter is ignored in this mock implementation. + * In real-world usage, {@code keyId} should be used to locate the + * corresponding RSA key in an HSM or KMS.

+ */ + @Override + public String sign(String content, String keyId) { + try { + PrivateKey priKey = getPrivateKey("RSA", config.get("privateKey").toString()); + Signature privateSignature = Signature.getInstance("SHA256WithRSA"); + privateSignature.initSign(priKey); + privateSignature.update(content.getBytes(StandardCharsets.UTF_8)); + byte[] signature = privateSignature.sign(); + return Base64.getEncoder().encodeToString(signature); + } catch (Exception ignored) { + } + return null; + } + + /** + * RSA-PSS signing is not implemented in this mock. + * + *

This method is intentionally left blank as the current test + * does not require RSA-PSS signing.

+ */ + @Override + public String signPSS(String content, String keyId) { + return ""; + } + + /** + * RSA decryption is not implemented in this mock. + * + *

This method is not required for transaction creation + * in the current test scenario.

+ */ + @Override + public byte[] decrypt(String content, String keyId, RSATypeEnum rsaType) { + return new byte[0]; + } + + /** + * Utility method to load an RSA private key from a Base64-encoded string. + * + *

This method is used only for test purposes to simulate + * an external signing system.

+ */ + private PrivateKey getPrivateKey(String algorithm, String privateKey) throws Exception { + KeyFactory keyFactory = KeyFactory.getInstance(algorithm); + byte[] privateKeyData = Base64.getDecoder().decode(privateKey.getBytes(StandardCharsets.UTF_8)); + return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyData)); + } + }; + + /** + * Register the ExternalRsaProvider so that the SDK delegates + * RSA signing operations to the external provider. + */ + RsaUtil.setExtProvider(mockRsaProvider); + + /** + * Build transaction creation request. + */ + CreateTransactionRequest createTransactionRequest = new CreateTransactionRequest(); + createTransactionRequest.setSourceAccountKey(config.get("accountKey").toString()); + createTransactionRequest.setSourceAccountType("VAULT_ACCOUNT"); + createTransactionRequest.setDestinationAccountType("ONE_TIME_ADDRESS"); + createTransactionRequest.setDestinationAddress(config.get("destinationAddress").toString()); + createTransactionRequest.setCoinKey("USDT_METACOMP_ERC20_ETHEREUM_SEPOLIA"); + createTransactionRequest.setTxAmount("0.001"); + createTransactionRequest.setTxFeeLevel("MIDDLE"); + createTransactionRequest.setCustomerRefId(UUID.randomUUID().toString()); + TxKeyResult createTransactionResponse = ServiceExecutor.execute(transactionApi.createTransactions(createTransactionRequest)); + System.out.println(String.format("transaction has been created, txKey: %s", createTransactionResponse.getTxKey())); + } + } From b734e9e5e6d93301d3cac5e8f111ca8f650eb607 Mon Sep 17 00:00:00 2001 From: Jiahj Date: Tue, 13 Jan 2026 17:35:26 +0800 Subject: [PATCH 2/2] feat: support self-managed key management via KeyProvider - Add KeyProvider interface for custom signing/decryption. - Support keyProvider injection in SafeheronConfig. - Decouple SDK core from RSA private key strings. - Update demo and terminology to "Self-Managed Key Management". --- .../client/DefaultPrivateKeyProvider.java | 47 +++++++++ .../com/safeheron/client/KeyProvider.java | 36 +++++++ .../client/config/SafeheronConfig.java | 28 +++++- .../client/converter/ConverterFactory.java | 2 +- .../client/converter/RequestInterceptor.java | 9 +- .../converter/ResponseBodyConverter.java | 11 ++- .../client/utils/ExternalRsaProvider.java | 79 --------------- .../com/safeheron/client/utils/RsaUtil.java | 14 --- .../demo/api/transaction/TransactionTest.java | 99 +++++++++---------- 9 files changed, 167 insertions(+), 158 deletions(-) create mode 100644 src/main/java/com/safeheron/client/DefaultPrivateKeyProvider.java create mode 100644 src/main/java/com/safeheron/client/KeyProvider.java delete mode 100644 src/main/java/com/safeheron/client/utils/ExternalRsaProvider.java diff --git a/src/main/java/com/safeheron/client/DefaultPrivateKeyProvider.java b/src/main/java/com/safeheron/client/DefaultPrivateKeyProvider.java new file mode 100644 index 0000000..c723d12 --- /dev/null +++ b/src/main/java/com/safeheron/client/DefaultPrivateKeyProvider.java @@ -0,0 +1,47 @@ +package com.safeheron.client; + +import com.safeheron.client.config.RSATypeEnum; +import com.safeheron.client.utils.RsaUtil; + +import java.util.Objects; + +/** + * Built-in default KeyProvider implementation that uses a local RSA private key. + * + * @author Jiahj + */ +public final class DefaultPrivateKeyProvider implements KeyProvider { + + private final String privateKey; + + public DefaultPrivateKeyProvider(String privateKey) { + this.privateKey = Objects.requireNonNull(privateKey, "privateKey must not be null"); + } + + @Override + public String sign(String content) { + try { + return RsaUtil.sign(content, privateKey); + } catch (Exception e) { + throw new RuntimeException("Failed to sign content with RSA", e); + } + } + + @Override + public String signPSS(String content) { + try { + return RsaUtil.signPSS(content, privateKey); + } catch (Exception e) { + throw new RuntimeException("Failed to sign content with RSA-PSS", e); + } + } + + @Override + public byte[] decrypt(String content, RSATypeEnum rsaType) { + try { + return RsaUtil.decrypt(content, privateKey, rsaType); + } catch (Exception e) { + throw new RuntimeException("Failed to decrypt content with RSA", e); + } + } +} diff --git a/src/main/java/com/safeheron/client/KeyProvider.java b/src/main/java/com/safeheron/client/KeyProvider.java new file mode 100644 index 0000000..b9531f9 --- /dev/null +++ b/src/main/java/com/safeheron/client/KeyProvider.java @@ -0,0 +1,36 @@ +package com.safeheron.client; + +import com.safeheron.client.config.RSATypeEnum; + +/** + * KeyProvider interface for signing and decryption capabilities. + * This is the single extension point for self-managed key solutions. + * + * @author Jiahj + */ +public interface KeyProvider { + /** + * Sign content using RSA (PKCS#1 v1.5) + * + * @param content the content to sign + * @return the Base64-encoded signature + */ + String sign(String content); + + /** + * Sign content using RSA-PSS + * + * @param content the content to sign + * @return the Base64-encoded RSA-PSS signature + */ + String signPSS(String content); + + /** + * Decrypt content using RSA + * + * @param content the Base64-encoded encrypted content + * @param rsaType the RSA algorithm type used for decryption + * @return the decrypted raw byte array + */ + byte[] decrypt(String content, RSATypeEnum rsaType); +} diff --git a/src/main/java/com/safeheron/client/config/SafeheronConfig.java b/src/main/java/com/safeheron/client/config/SafeheronConfig.java index 87c787f..0852b9e 100644 --- a/src/main/java/com/safeheron/client/config/SafeheronConfig.java +++ b/src/main/java/com/safeheron/client/config/SafeheronConfig.java @@ -1,5 +1,7 @@ package com.safeheron.client.config; +import com.safeheron.client.DefaultPrivateKeyProvider; +import com.safeheron.client.KeyProvider; import lombok.Builder; import lombok.Data; @@ -12,25 +14,49 @@ public class SafeheronConfig { /** * Safeheron Request Base URL */ + @Builder.Default private String baseUrl = ""; /** * api key, you can get from safeheron web console */ + @Builder.Default private String apiKey = ""; /** - * Your RSA private key + * Your RSA private key. + *

In Self-Managed Key Management scenarios, this field can also be used to store + * a key identifier (keyId), which your custom {@link com.safeheron.client.KeyProvider} + * can use to locate the actual key in an external system.

*/ + @Builder.Default private String rsaPrivateKey = ""; + /** + * KeyProvider for signing and decryption. + * If both keyProvider and rsaPrivateKey are provided, keyProvider takes precedence. + */ + private KeyProvider keyProvider; + /** * Api key's platform public key, you can get from safeheron web console */ + @Builder.Default private String safeheronRsaPublicKey = ""; /** * requestTimeout */ + @Builder.Default private Long requestTimeout = 20000L; + + public KeyProvider getKeyProvider() { + if (keyProvider != null) { + return keyProvider; + } + if (rsaPrivateKey != null && !rsaPrivateKey.isEmpty()) { + return new DefaultPrivateKeyProvider(rsaPrivateKey); + } + return null; + } } diff --git a/src/main/java/com/safeheron/client/converter/ConverterFactory.java b/src/main/java/com/safeheron/client/converter/ConverterFactory.java index 10a0fbb..af596e6 100644 --- a/src/main/java/com/safeheron/client/converter/ConverterFactory.java +++ b/src/main/java/com/safeheron/client/converter/ConverterFactory.java @@ -35,7 +35,7 @@ private ConverterFactory(SafeheronConfig config) { JavaType javaType = mapper.getTypeFactory().constructType(type); ObjectReader reader = mapper.readerFor(javaType); return new ResponseBodyConverter<>(reader, - config.getSafeheronRsaPublicKey(), config.getRsaPrivateKey()); + config.getSafeheronRsaPublicKey(), config.getKeyProvider()); } @Override diff --git a/src/main/java/com/safeheron/client/converter/RequestInterceptor.java b/src/main/java/com/safeheron/client/converter/RequestInterceptor.java index c4888b3..e60ece1 100644 --- a/src/main/java/com/safeheron/client/converter/RequestInterceptor.java +++ b/src/main/java/com/safeheron/client/converter/RequestInterceptor.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; +import com.safeheron.client.KeyProvider; import com.safeheron.client.config.AESTypeEnum; import com.safeheron.client.config.RSATypeEnum; import com.safeheron.client.config.SafeheronConfig; @@ -27,7 +28,7 @@ public class RequestInterceptor implements Interceptor { private final ObjectWriter objectWriter; private final String apiKey; private final String safeheronRsaPublicKey; - private final String rsaPrivateKey; + private final KeyProvider keyProvider; public static RequestInterceptor create(SafeheronConfig config) { @@ -38,7 +39,7 @@ private RequestInterceptor(SafeheronConfig config) { this.objectWriter = mapper.writerFor(Map.class); this.apiKey = config.getApiKey(); this.safeheronRsaPublicKey = config.getSafeheronRsaPublicKey(); - this.rsaPrivateKey = config.getRsaPrivateKey(); + this.keyProvider = config.getKeyProvider(); } @NotNull @@ -73,11 +74,11 @@ public Response intercept(@NotNull Chain chain) throws IOException { } requestData.put("key", rsaEncryptResult); - // Sign the request data with your RSA private key + // Sign the request data with your KeyProvider String signContent = requestData.entrySet().stream() .map(entry -> entry.getKey() + "=" + entry.getValue()) .collect(Collectors.joining("&")); - String rsaSig = RsaUtil.sign(signContent, rsaPrivateKey); + String rsaSig = keyProvider.sign(signContent); requestData.put("sig", rsaSig); requestData.put("rsaType", RSATypeEnum.ECB_OAEP.getCode()); requestData.put("aesType", AESTypeEnum.GCM.getCode()); diff --git a/src/main/java/com/safeheron/client/converter/ResponseBodyConverter.java b/src/main/java/com/safeheron/client/converter/ResponseBodyConverter.java index 65a6fc3..821bc2d 100644 --- a/src/main/java/com/safeheron/client/converter/ResponseBodyConverter.java +++ b/src/main/java/com/safeheron/client/converter/ResponseBodyConverter.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; +import com.safeheron.client.KeyProvider; import com.safeheron.client.config.AESTypeEnum; import com.safeheron.client.config.RSATypeEnum; import com.safeheron.client.exception.SafeheronException; @@ -28,13 +29,13 @@ public class ResponseBodyConverter implements Converter { private final ObjectReader reader ; private final String safeheronRsaPublicKey; - private final String rsaPrivateKey; + private final KeyProvider keyProvider; ResponseBodyConverter(ObjectReader reader, - String safeheronRsaPublicKey, String rsaPrivateKey) { + String safeheronRsaPublicKey, KeyProvider keyProvider) { this.reader = reader; this.safeheronRsaPublicKey = safeheronRsaPublicKey; - this.rsaPrivateKey = rsaPrivateKey; + this.keyProvider = keyProvider; } @Override @@ -66,9 +67,9 @@ public T convert(ResponseBody value) throws IOException { throw new SafeheronException("response signature verification failed"); } - // Use your RSA private key to decrypt response's aesKey and aesIv + // Use your KeyProvider to decrypt response's aesKey and aesIv RSATypeEnum rsaType = StringUtils.isNotEmpty(apiResult.getRsaType()) && RSATypeEnum.valueByCode(apiResult.getRsaType()) != null ? RSATypeEnum.valueByCode(apiResult.getRsaType()) : RSATypeEnum.RSA; - byte[] aesSaltDecrypt = RsaUtil.decrypt(apiResult.getKey(), rsaPrivateKey,rsaType); + byte[] aesSaltDecrypt = keyProvider.decrypt(apiResult.getKey(), rsaType); byte[] aesKey = Arrays.copyOfRange(aesSaltDecrypt, 0, 32); byte[] iv = Arrays.copyOfRange(aesSaltDecrypt, 32, aesSaltDecrypt.length); diff --git a/src/main/java/com/safeheron/client/utils/ExternalRsaProvider.java b/src/main/java/com/safeheron/client/utils/ExternalRsaProvider.java deleted file mode 100644 index df6f4ae..0000000 --- a/src/main/java/com/safeheron/client/utils/ExternalRsaProvider.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.safeheron.client.utils; - -import com.safeheron.client.config.RSATypeEnum; - -/** - * External RSA provider interface for delegating RSA cryptographic operations - * to external key management or signing systems. - * - *

This interface is designed to support scenarios where RSA private keys - * are managed by secure infrastructures such as HSM, KMS, or other - * centralized key management services. In such environments, private key - * material must never be exported or exposed to the application layer.

- * - *

By introducing this abstraction, the SDK can delegate RSA signing and - * decryption operations to external systems via a {@code keyId}, instead of - * requiring the complete private key to be configured locally.

- * - *

Typical use cases include:

- *
    - *
  • Private keys stored in HSM or cloud KMS (AWS KMS, Azure Key Vault, etc.)
  • - *
  • Compliance-driven environments where private keys are prohibited from leaving secure boundaries
  • - *
  • Custom or enterprise-grade signing services
  • - *
- * - *

Implementations of this interface are responsible for:

- *
    - *
  • Resolving the {@code keyId} to the actual RSA key in the external system
  • - *
  • Executing the appropriate RSA algorithm (PKCS#1 v1.5, RSA-PSS, etc.)
  • - *
  • Ensuring cryptographic correctness and security of the operation
  • - *
- * - *

The SDK itself does not hold, persist, or process any RSA private key - * material when this provider is used.

- * - * @author Jiahj - */ -public interface ExternalRsaProvider { - /** - * Sign the given content using RSA (PKCS#1 v1.5) with a key managed by an external system - * such as HSM or KMS. - * - *

The actual private key material must never be exposed to the SDK. - * Implementations should perform the signing operation by delegating to - * the external key management or signing service identified by {@code keyId}.

- * - * @param content the plain text content to be signed - * @param keyId the identifier of the RSA key in the external signing system - * @return the Base64-encoded signature result - */ - String sign(String content, String keyId); - - /** - * Sign the given content using RSA-PSS with a key managed by an external system - * such as HSM or KMS. - * - *

This method is intended for scenarios requiring stronger cryptographic - * security guarantees compared to PKCS#1 v1.5.

- * - * @param content the plain text content to be signed - * @param keyId the identifier of the RSA key in the external signing system - * @return the Base64-encoded RSA-PSS signature result - */ - String signPSS(String content, String keyId); - - /** - * Decrypt the given encrypted content using an RSA private key managed by - * an external system such as HSM or KMS. - * - *

The decryption operation must be performed by the external key management - * system, and the private key material must not be exposed to the SDK.

- * - * @param content the encrypted content, typically Base64-encoded - * @param keyId the identifier of the RSA key in the external key management system - * @param rsaType the RSA algorithm type used for decryption - * @return the decrypted raw byte array - */ - byte[] decrypt(String content, String keyId, RSATypeEnum rsaType); - -} diff --git a/src/main/java/com/safeheron/client/utils/RsaUtil.java b/src/main/java/com/safeheron/client/utils/RsaUtil.java index ae72715..6e23c63 100644 --- a/src/main/java/com/safeheron/client/utils/RsaUtil.java +++ b/src/main/java/com/safeheron/client/utils/RsaUtil.java @@ -26,16 +26,11 @@ public class RsaUtil { public static final String SIGN_ALGORITHMS_SHA256RSA_PSS = "SHA256withRSA/PSS"; private static final int MAX_ENCRYPT_BLOCK = 501; private static final int MAX_DECRYPT_BLOCK = 512; - private static ExternalRsaProvider externalRsaProvider; static { Security.addProvider(new BouncyCastleProvider()); } - public static void setExtProvider(ExternalRsaProvider lExternalRsaProvider) { - externalRsaProvider = lExternalRsaProvider; - } - public static String encrypt(byte[] plainText, String publicKey, RSATypeEnum RSAType) throws Exception { ByteArrayOutputStream out = null; try { @@ -73,9 +68,6 @@ public static String encrypt(byte[] plainText, String publicKey, RSATypeEnum RSA } public static byte[] decrypt(String content, String privateKey, RSATypeEnum RSAType) throws Exception { - if (externalRsaProvider != null) { - return externalRsaProvider.decrypt(content, privateKey, RSAType); - } ByteArrayOutputStream out = null; try { PrivateKey priKey = getPrivateKey(SIGN_TYPE_RSA, privateKey); @@ -114,9 +106,6 @@ public static byte[] decrypt(String content, String privateKey, RSATypeEnum RSAT } public static String sign(String content, String privateKey) throws Exception { - if (externalRsaProvider != null) { - return externalRsaProvider.sign(content, privateKey); - } PrivateKey priKey = getPrivateKey(SIGN_TYPE_RSA, privateKey); Signature privateSignature = Signature.getInstance(SIGN_ALGORITHMS_SHA256RSA); privateSignature.initSign(priKey); @@ -126,9 +115,6 @@ public static String sign(String content, String privateKey) throws Exception { } public static String signPSS(String content, String privateKey) throws Exception { - if (externalRsaProvider != null) { - return externalRsaProvider.signPSS(content, privateKey); - } PrivateKey priKey = getPrivateKey(SIGN_TYPE_RSA, privateKey); Signature privateSignature = Signature.getInstance(SIGN_ALGORITHMS_SHA256RSA_PSS); privateSignature.initSign(priKey); diff --git a/src/test/java/com/safeheron/demo/api/transaction/TransactionTest.java b/src/test/java/com/safeheron/demo/api/transaction/TransactionTest.java index 6e693b3..37d0e51 100644 --- a/src/test/java/com/safeheron/demo/api/transaction/TransactionTest.java +++ b/src/test/java/com/safeheron/demo/api/transaction/TransactionTest.java @@ -1,11 +1,11 @@ package com.safeheron.demo.api.transaction; +import com.safeheron.client.KeyProvider; import com.safeheron.client.api.TransactionApiService; import com.safeheron.client.config.RSATypeEnum; import com.safeheron.client.config.SafeheronConfig; import com.safeheron.client.request.CreateTransactionRequest; import com.safeheron.client.response.TxKeyResult; -import com.safeheron.client.utils.ExternalRsaProvider; import com.safeheron.client.utils.RsaUtil; import com.safeheron.client.utils.ServiceCreator; import com.safeheron.client.utils.ServiceExecutor; @@ -70,93 +70,84 @@ public void testSendTransaction(){ } /** - * Test case for sending a transaction using an ExternalRsaProvider. + * Test case for sending a transaction using a custom KeyProvider. * - *

This test demonstrates how to integrate a custom {@link ExternalRsaProvider} - * to perform RSA signing through an external system (e.g. HSM / KMS), - * instead of configuring the complete RSA private key in the SDK.

+ *

This test demonstrates how to integrate a custom {@link KeyProvider} + * to perform RSA signing through an external system (e.g. Self-Managed Key Management). * - *

For demonstration purposes, this test uses a mock implementation - * of {@code ExternalRsaProvider} that performs local RSA signing. - * In real production scenarios, the signing logic should be delegated - * to a secure key management or signing service, identified by {@code keyId}.

+ *

In this scenario, we demonstrate that the "privateKey" field in config.yaml + * can actually be a "keyId" or "keyName" used by your KMS, rather than the actual private key.

*/ @Test - public void testSendTransactionWithExternalRsaProvider() { + public void testSendTransactionWithKeyProvider() { + // 1. Assume we got a keyId from our configuration (e.g., config.yaml) + // Although the key in YAML is named 'privateKey', its content could be a 'keyId' + String keyIdFromConfig = config.get("privateKey").toString(); /** - * Mock implementation of ExternalRsaProvider. - * - *

This implementation simulates an external signing system by - * performing RSA signing locally using a private key loaded from - * configuration. It is intended for testing and demonstration only.

+ * 2. Define a Self-Managed KeyProvider. + * In a real project, this class would be part of your application code. */ - ExternalRsaProvider mockRsaProvider = new ExternalRsaProvider() { - - /** - * Sign the given content using RSA (SHA256withRSA). - * - *

The {@code keyId} parameter is ignored in this mock implementation. - * In real-world usage, {@code keyId} should be used to locate the - * corresponding RSA key in an HSM or KMS.

- */ + class MySelfManagedKeyProvider implements KeyProvider { + private final String keyId; + + public MySelfManagedKeyProvider(String keyId) { + this.keyId = keyId; + } + @Override - public String sign(String content, String keyId) { + public String sign(String content) { + // In a real scenario, you would call your signing service: + // return mySigningService.sign(content, this.keyId); + + // For this test to pass, we simulate the signing service by signing locally + // using the keyId (which we know is actually the private key in this demo) try { - PrivateKey priKey = getPrivateKey("RSA", config.get("privateKey").toString()); + PrivateKey priKey = getPrivateKey("RSA", this.keyId); Signature privateSignature = Signature.getInstance("SHA256WithRSA"); privateSignature.initSign(priKey); privateSignature.update(content.getBytes(StandardCharsets.UTF_8)); byte[] signature = privateSignature.sign(); return Base64.getEncoder().encodeToString(signature); - } catch (Exception ignored) { + } catch (Exception e) { + throw new RuntimeException("Signing failed", e); } - return null; } - /** - * RSA-PSS signing is not implemented in this mock. - * - *

This method is intentionally left blank as the current test - * does not require RSA-PSS signing.

- */ @Override - public String signPSS(String content, String keyId) { + public String signPSS(String content) { return ""; } - /** - * RSA decryption is not implemented in this mock. - * - *

This method is not required for transaction creation - * in the current test scenario.

- */ @Override - public byte[] decrypt(String content, String keyId, RSATypeEnum rsaType) { + public byte[] decrypt(String content, RSATypeEnum rsaType) { return new byte[0]; } - /** - * Utility method to load an RSA private key from a Base64-encoded string. - * - *

This method is used only for test purposes to simulate - * an external signing system.

- */ private PrivateKey getPrivateKey(String algorithm, String privateKey) throws Exception { KeyFactory keyFactory = KeyFactory.getInstance(algorithm); byte[] privateKeyData = Base64.getDecoder().decode(privateKey.getBytes(StandardCharsets.UTF_8)); return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyData)); } - }; + } + + // 3. Instantiate your provider with the keyId + KeyProvider mySelfManagedProvider = new MySelfManagedKeyProvider(keyIdFromConfig); /** - * Register the ExternalRsaProvider so that the SDK delegates - * RSA signing operations to the external provider. + * 4. Build TransactionApiService with the custom KeyProvider. + * Notice we don't call .rsaPrivateKey() here, as the provider handles everything. */ - RsaUtil.setExtProvider(mockRsaProvider); + TransactionApiService transactionApiWithKeyProvider = ServiceCreator.create(TransactionApiService.class, SafeheronConfig.builder() + .baseUrl(config.get("baseUrl").toString()) + .apiKey(config.get("apiKey").toString()) + .safeheronRsaPublicKey(config.get("safeheronPublicKey").toString()) + .keyProvider(mySelfManagedProvider) + .requestTimeout(Long.valueOf(config.get("requestTimeout").toString())) + .build()); /** - * Build transaction creation request. + * 5. Execute transaction creation as usual. */ CreateTransactionRequest createTransactionRequest = new CreateTransactionRequest(); createTransactionRequest.setSourceAccountKey(config.get("accountKey").toString()); @@ -167,7 +158,7 @@ private PrivateKey getPrivateKey(String algorithm, String privateKey) throws Exc createTransactionRequest.setTxAmount("0.001"); createTransactionRequest.setTxFeeLevel("MIDDLE"); createTransactionRequest.setCustomerRefId(UUID.randomUUID().toString()); - TxKeyResult createTransactionResponse = ServiceExecutor.execute(transactionApi.createTransactions(createTransactionRequest)); + TxKeyResult createTransactionResponse = ServiceExecutor.execute(transactionApiWithKeyProvider.createTransactions(createTransactionRequest)); System.out.println(String.format("transaction has been created, txKey: %s", createTransactionResponse.getTxKey())); }