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 ResponseBodyConverterThis test demonstrates how to integrate a custom {@link KeyProvider} + * to perform RSA signing through an external system (e.g. Self-Managed Key Management). + * + *
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 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(); + + /** + * 2. Define a Self-Managed KeyProvider. + * In a real project, this class would be part of your application code. + */ + class MySelfManagedKeyProvider implements KeyProvider { + private final String keyId; + + public MySelfManagedKeyProvider(String keyId) { + this.keyId = keyId; + } + + @Override + 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", 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 e) { + throw new RuntimeException("Signing failed", e); + } + } + + @Override + public String signPSS(String content) { + return ""; + } + + @Override + public byte[] decrypt(String content, RSATypeEnum rsaType) { + return new byte[0]; + } + + 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); + + /** + * 4. Build TransactionApiService with the custom KeyProvider. + * Notice we don't call .rsaPrivateKey() here, as the provider handles everything. + */ + 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()); + + /** + * 5. Execute transaction creation as usual. + */ + 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(transactionApiWithKeyProvider.createTransactions(createTransactionRequest)); + System.out.println(String.format("transaction has been created, txKey: %s", createTransactionResponse.getTxKey())); + } + }