Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions src/main/java/com/safeheron/client/DefaultPrivateKeyProvider.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
36 changes: 36 additions & 0 deletions src/main/java/com/safeheron/client/KeyProvider.java
Original file line number Diff line number Diff line change
@@ -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);
}
28 changes: 27 additions & 1 deletion src/main/java/com/safeheron/client/config/SafeheronConfig.java
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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.
* <p>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.</p>
*/
@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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand All @@ -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
Expand Down Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -28,13 +29,13 @@ public class ResponseBodyConverter<T> implements Converter<ResponseBody, T> {
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
Expand Down Expand Up @@ -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);

Expand Down
102 changes: 102 additions & 0 deletions src/test/java/com/safeheron/demo/api/transaction/TransactionTest.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
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.RsaUtil;
import com.safeheron.client.utils.ServiceCreator;
import com.safeheron.client.utils.ServiceExecutor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -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;

Expand Down Expand Up @@ -60,4 +69,97 @@ public void testSendTransaction(){
System.out.println(String.format("transaction has been created, txKey: %s", createTransactionResponse.getTxKey()));
}

/**
* Test case for sending a transaction using a custom KeyProvider.
*
* <p>This test demonstrates how to integrate a custom {@link KeyProvider}
* to perform RSA signing through an external system (e.g. Self-Managed Key Management).
*
* <p>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.</p>
*/
@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()));
}

}