diff --git a/pom.xml b/pom.xml
index 5b44e61..7698688 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
ai.pluggy
pluggy-java
- 0.15.1
+ 0.16.0
jar
diff --git a/src/main/java/ai/pluggy/client/PluggyApiService.java b/src/main/java/ai/pluggy/client/PluggyApiService.java
index 9e2d699..13dfae5 100644
--- a/src/main/java/ai/pluggy/client/PluggyApiService.java
+++ b/src/main/java/ai/pluggy/client/PluggyApiService.java
@@ -35,7 +35,6 @@
import retrofit2.http.QueryMap;
public interface PluggyApiService {
-
@GET("/connectors")
Call getConnectors();
diff --git a/src/main/java/ai/pluggy/client/PluggyClient.java b/src/main/java/ai/pluggy/client/PluggyClient.java
index 1d46734..e363dff 100644
--- a/src/main/java/ai/pluggy/client/PluggyClient.java
+++ b/src/main/java/ai/pluggy/client/PluggyClient.java
@@ -5,6 +5,7 @@
import ai.pluggy.client.auth.ApiKeyAuthInterceptor;
import ai.pluggy.client.auth.AuthenticationHelper;
+import ai.pluggy.client.auth.EncryptedParametersInterceptor;
import ai.pluggy.client.response.ErrorResponse;
import ai.pluggy.exception.PluggyException;
import ai.pluggy.utils.Utils;
@@ -28,14 +29,11 @@
import retrofit2.converter.gson.GsonConverterFactory;
public final class PluggyClient {
-
public static final int ONE_MIB_BYTES = 1024 * 1024;
public static String AUTH_URL_PATH = "/auth";
private String baseUrl;
-
private String clientId;
private String clientSecret;
-
private OkHttpClient httpClient;
private PluggyApiService service;
@@ -156,6 +154,7 @@ public static class PluggyClientBuilder {
private String clientId;
private String clientSecret;
private String baseUrl;
+ private String rsaPublicKey;
private Builder okHttpClientBuilder;
private boolean disableDefaultAuthInterceptor = false;
@@ -180,6 +179,11 @@ public PluggyClientBuilder baseUrl(String baseUrl) {
return this;
}
+ public PluggyClientBuilder rsaPublicKey(String rsaPublicKey) {
+ this.rsaPublicKey = rsaPublicKey;
+ return this;
+ }
+
/**
* Opt-out from provided default ApiKeyAuthInterceptor, which takes care of apiKey
* authorization, by requesting a new apiKey token when it's not set, or by reactively
@@ -211,6 +215,10 @@ private OkHttpClient buildOkHttpClient(String baseUrl) {
.addInterceptor(new ApiKeyAuthInterceptor(authUrlPath, clientId, clientSecret));
}
+ if (this.rsaPublicKey != null) {
+ this.okHttpClientBuilder.addInterceptor(new EncryptedParametersInterceptor(this.rsaPublicKey));
+ }
+
return okHttpClientBuilder.build();
}
@@ -278,7 +286,7 @@ public String authenticate() throws IOException, PluggyException {
.post(body)
.addHeader("content-type", "application/json")
.addHeader("cache-control", "no-cache")
- .addHeader("User-Agent", "PluggyJava/0.15.1")
+ .addHeader("User-Agent", "PluggyJava/0.16.0")
.build();
String apiKey;
diff --git a/src/main/java/ai/pluggy/client/auth/ApiKeyAuthInterceptor.java b/src/main/java/ai/pluggy/client/auth/ApiKeyAuthInterceptor.java
index 75825a3..1e756e6 100644
--- a/src/main/java/ai/pluggy/client/auth/ApiKeyAuthInterceptor.java
+++ b/src/main/java/ai/pluggy/client/auth/ApiKeyAuthInterceptor.java
@@ -2,7 +2,6 @@
import static ai.pluggy.utils.Asserts.assertNotNull;
-import ai.pluggy.utils.Utils;
import com.google.gson.Gson;
import java.io.IOException;
@@ -116,7 +115,7 @@ private Request requestWithAuth(Request originalRequest, String apiKey) {
return originalRequest.newBuilder()
.header(X_API_KEY_HEADER, apiKey)
// TOOD: add dynamic version
- .header("User-Agent", "PluggyJava/0.15.1")
+ .header("User-Agent", "PluggyJava/0.16.0")
.build();
}
diff --git a/src/main/java/ai/pluggy/client/auth/EncryptedParametersInterceptor.java b/src/main/java/ai/pluggy/client/auth/EncryptedParametersInterceptor.java
new file mode 100644
index 0000000..03a483c
--- /dev/null
+++ b/src/main/java/ai/pluggy/client/auth/EncryptedParametersInterceptor.java
@@ -0,0 +1,109 @@
+package ai.pluggy.client.auth;
+
+import static ai.pluggy.utils.Asserts.assertNotNull;
+
+import java.io.IOException;
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+
+import okio.Buffer;
+import java.util.Arrays;
+import java.util.Base64;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+
+import org.jetbrains.annotations.NotNull;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+
+import okhttp3.Interceptor;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+
+public class EncryptedParametersInterceptor implements Interceptor {
+ private String rsaPublicKey;
+ private String path = "/items";
+ private String[] methods = { "PATCH", "POST" };
+
+ public EncryptedParametersInterceptor(String rsaPublicKey) {
+ assertNotNull(rsaPublicKey, rsaPublicKey);
+ this.rsaPublicKey = rsaPublicKey;
+ }
+
+ public Response intercept(@NotNull Chain chain) throws IOException {
+ Request originalRequest = chain.request();
+ String method = originalRequest.method();
+ RequestBody originalBody = originalRequest.body();
+
+ if (originalBody == null) {
+ return chain.proceed(originalRequest);
+ }
+
+ JsonObject jsonBody = this.transformBodyToJsonObject(originalBody);
+
+ if (!Arrays.asList(methods).contains(method)
+ || !originalRequest.url().encodedPath().contains(path) && !jsonBody.has("parameters")) {
+ return chain.proceed(originalRequest);
+ }
+
+ String parameters = jsonBody.get("parameters").toString();
+
+ String encryptedParameters = encryptParameters(parameters);
+
+ jsonBody.addProperty("parameters", encryptedParameters);
+
+ // create new request with new body and same headers/params
+ Request newRequest = originalRequest.newBuilder()
+ .method(method, RequestBody.create(jsonBody.toString(), originalBody.contentType()))
+ .build();
+
+ return chain.proceed(newRequest);
+ }
+
+ private String encryptParameters(String parameters) throws IOException {
+ String publicKeyPEM = this.rsaPublicKey
+ .replace("-----BEGIN PUBLIC KEY-----", "")
+ .replace("-----END PUBLIC KEY-----", "")
+ .replaceAll("\\s+", "");
+
+ byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyPEM);
+ X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKeyBytes);
+
+ try {
+ KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+ PublicKey publicKey = keyFactory.generatePublic(keySpec);
+ Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPPadding");
+ cipher.init(Cipher.ENCRYPT_MODE, publicKey);
+ byte[] encryptedPayloadBytes = cipher.doFinal(parameters.getBytes());
+ String encryptedPayload = Base64.getEncoder().encodeToString(encryptedPayloadBytes);
+ return encryptedPayload;
+
+ } catch (NoSuchAlgorithmException | InvalidKeySpecException | NoSuchPaddingException | InvalidKeyException
+ | BadPaddingException | IllegalBlockSizeException error) {
+ throw new IOException("Error encrypting parameters", error);
+ }
+ }
+
+ private JsonObject transformBodyToJsonObject(RequestBody body) throws IOException {
+ Buffer buffer = new Buffer();
+ try {
+ body.writeTo(buffer);
+ } catch (IOException error) {
+ throw error;
+ }
+ String bodyString = buffer.readUtf8();
+
+ JsonObject jsonBody = new Gson().fromJson(bodyString, JsonObject.class);
+
+ return jsonBody;
+ }
+}
diff --git a/src/main/java/ai/pluggy/client/request/CreateItemRequest.java b/src/main/java/ai/pluggy/client/request/CreateItemRequest.java
index 8645296..0180ed3 100644
--- a/src/main/java/ai/pluggy/client/request/CreateItemRequest.java
+++ b/src/main/java/ai/pluggy/client/request/CreateItemRequest.java
@@ -6,7 +6,6 @@
@Value
@AllArgsConstructor
public class CreateItemRequest {
-
Integer connectorId;
ParametersMap parameters;
String webhookUrl;
diff --git a/src/main/java/ai/pluggy/client/response/Account.java b/src/main/java/ai/pluggy/client/response/Account.java
index 6c209e7..708d4cd 100644
--- a/src/main/java/ai/pluggy/client/response/Account.java
+++ b/src/main/java/ai/pluggy/client/response/Account.java
@@ -1,8 +1,5 @@
package ai.pluggy.client.response;
-
-import java.util.ArrayList;
-import java.util.List;
import lombok.Data;
@Data
diff --git a/src/main/java/ai/pluggy/client/response/WebhookEventType.java b/src/main/java/ai/pluggy/client/response/WebhookEventType.java
index 446d6ca..99c308e 100644
--- a/src/main/java/ai/pluggy/client/response/WebhookEventType.java
+++ b/src/main/java/ai/pluggy/client/response/WebhookEventType.java
@@ -1,8 +1,6 @@
package ai.pluggy.client.response;
import com.google.gson.annotations.SerializedName;
-import lombok.AllArgsConstructor;
-import lombok.Getter;
public enum WebhookEventType {
@SerializedName("item/created")
diff --git a/src/main/java/ai/pluggy/exception/PluggyException.java b/src/main/java/ai/pluggy/exception/PluggyException.java
index 0b64976..73b8c79 100644
--- a/src/main/java/ai/pluggy/exception/PluggyException.java
+++ b/src/main/java/ai/pluggy/exception/PluggyException.java
@@ -1,6 +1,5 @@
package ai.pluggy.exception;
-import java.io.IOException;
import okhttp3.Response;
public class PluggyException extends Exception {
diff --git a/src/test/java/ai/pluggy/client/integration/BaseApiIntegrationTest.java b/src/test/java/ai/pluggy/client/integration/BaseApiIntegrationTest.java
index 03417ad..0bb55b4 100644
--- a/src/test/java/ai/pluggy/client/integration/BaseApiIntegrationTest.java
+++ b/src/test/java/ai/pluggy/client/integration/BaseApiIntegrationTest.java
@@ -11,13 +11,13 @@
import org.junit.platform.commons.util.StringUtils;
class BaseApiIntegrationTest {
-
static final String CLIENT_ID = System.getenv("PLUGGY_CLIENT_ID");
static final String CLIENT_SECRET = System.getenv("PLUGGY_CLIENT_SECRET");
static final String TEST_BASE_URL = System.getenv("PLUGGY_BASE_URL");
+ static final String RSA_PUBLIC_KEY = System.getenv("PLUGGY_RSA_PUBLIC_KEY");
PluggyClient client;
-
+
protected List itemsIdCreated = new ArrayList<>();
public List getItemsIdCreated() {
@@ -28,9 +28,9 @@ public List getItemsIdCreated() {
void setUp() {
checkEnvErrors();
client = PluggyClient.builder()
- .baseUrl(TEST_BASE_URL)
- .clientIdAndSecret(CLIENT_ID, CLIENT_SECRET)
- .build();
+ .baseUrl(TEST_BASE_URL)
+ .clientIdAndSecret(CLIENT_ID, CLIENT_SECRET)
+ .build();
}
protected void checkEnvErrors() {
@@ -46,12 +46,12 @@ protected void checkEnvErrors() {
}
if (missingEnvVars.size() > 0) {
String envVarsListString = missingEnvVars.stream()
- .map(varName -> "'" + varName + "'")
- .collect(Collectors.joining(", "));
+ .map(varName -> "'" + varName + "'")
+ .collect(Collectors.joining(", "));
throw new IllegalStateException("Must define " + envVarsListString + " env var(s)!");
}
}
-
+
@SneakyThrows
@AfterEach
protected void clearItems() {
diff --git a/src/test/java/ai/pluggy/client/integration/CreateItemTest.java b/src/test/java/ai/pluggy/client/integration/CreateItemTest.java
index 0f6d61d..09513bd 100644
--- a/src/test/java/ai/pluggy/client/integration/CreateItemTest.java
+++ b/src/test/java/ai/pluggy/client/integration/CreateItemTest.java
@@ -7,6 +7,7 @@
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import ai.pluggy.client.PluggyClient;
import ai.pluggy.client.request.CreateItemRequest;
import ai.pluggy.client.request.ParametersMap;
import ai.pluggy.client.response.ErrorResponse;
@@ -17,13 +18,12 @@
import retrofit2.Response;
public class CreateItemTest extends BaseApiIntegrationTest {
-
@SneakyThrows
@Test
void createItem_withValidParams_responseOk() {
// create item params
ParametersMap parametersMap = ParametersMap.map("user", "user-ok")
- .with("password", "password-ok");
+ .with("password", "password-ok");
Integer connectorId = 0;
// run request with 'connectorId', 'parameters' params
@@ -37,20 +37,21 @@ void createItem_withValidParams_responseOk() {
assertNotNull(itemResponse1);
assertEquals(itemResponse1.getConnector().getId(), connectorId);
- // run request with 'connectorId', 'parameters', 'webhookUrl', 'clientUserId', params
+ // run request with 'connectorId', 'parameters', 'webhookUrl', 'clientUserId',
+ // params
String webhookUrl = "https://www.test.com/";
String clientUserId = "clientUserId";
CreateItemRequest createItemRequestWithWebhook = new CreateItemRequest(connectorId,
- parametersMap, webhookUrl, clientUserId);
+ parametersMap, webhookUrl, clientUserId);
Response itemRequestWithWebhookResponse = client.service()
- .createItem(createItemRequestWithWebhook).execute();
+ .createItem(createItemRequestWithWebhook).execute();
ItemResponse itemResponse2 = itemRequestWithWebhookResponse.body();
assertNotNull(itemResponse2);
assertEquals(itemResponse2.getConnector().getId(), connectorId);
assertEquals(itemResponse2.getClientUserId(), clientUserId);
assertEquals(itemResponse2.getWebhookUrl(), webhookUrl);
-
+
this.getItemsIdCreated().add(itemResponse1.getId());
this.getItemsIdCreated().add(itemResponse2.getId());
}
@@ -60,8 +61,8 @@ void createItem_withValidParams_responseOk() {
void createItem_withInvalidParams_responseError400() {
// create item params
ParametersMap parametersMap = ParametersMap
- .map("bad-param-key", "asd")
- .with("other-bad-param-key", "qwe");
+ .map("bad-param-key", "asd")
+ .with("other-bad-param-key", "qwe");
Integer connectorId = 0;
// run request with 'connectorId', 'parameters' params
@@ -81,9 +82,9 @@ void createItem_withInvalidParams_responseError400() {
// webhookUrl param 'localhost' is invalid, expect error 400
String localWebhookUrl = "localhost:9999";
CreateItemRequest createItemRequestWithLocalWebhook = new CreateItemRequest(connectorId,
- parametersMap, localWebhookUrl);
+ parametersMap, localWebhookUrl);
Response itemRequestWithLocalWebhookResponse = client.service()
- .createItem(createItemRequestWithLocalWebhook).execute();
+ .createItem(createItemRequestWithLocalWebhook).execute();
ErrorResponse localWebhookErrorResponse = client.parseError(itemRequestWithLocalWebhookResponse);
assertNotNull(localWebhookErrorResponse);
assertEquals(localWebhookErrorResponse.getCode(), 400);
@@ -92,12 +93,40 @@ void createItem_withInvalidParams_responseError400() {
// webhookUrl param http is invalid, expect error 400
String httpWebhookUrl = "http://www.test.com";
CreateItemRequest createItemRequestWithHttpWebhook = new CreateItemRequest(connectorId,
- parametersMap, httpWebhookUrl);
+ parametersMap, httpWebhookUrl);
Response itemRequestWithHttpWebhookResponse = client.service()
- .createItem(createItemRequestWithHttpWebhook).execute();
+ .createItem(createItemRequestWithHttpWebhook).execute();
ErrorResponse httpWebhookErrorResponse = client.parseError(itemRequestWithHttpWebhookResponse);
assertNotNull(httpWebhookErrorResponse);
assertEquals(httpWebhookErrorResponse.getCode(), 400);
assertNull(localWebhookErrorResponse.getDetails(), "should not include validation error details for webhookUrl");
}
+
+ @SneakyThrows
+ @Test
+ void createItem_withEncryptedParameters_responseOk() {
+ // create item params
+ ParametersMap parametersMap = ParametersMap.map("user", "user-ok")
+ .with("password", "password-ok");
+ Integer connectorId = 0;
+
+ // initialize client with RSA public key so the parameters are encrypted before
+ // sending to the API
+ PluggyClient clientWithEncryption = PluggyClient.builder()
+ .clientIdAndSecret(BaseApiIntegrationTest.CLIENT_ID, BaseApiIntegrationTest.CLIENT_SECRET)
+ .rsaPublicKey(BaseApiIntegrationTest.RSA_PUBLIC_KEY)
+ .build();
+
+ // run request with 'connectorId', 'parameters' params
+ CreateItemRequest createItemRequest = new CreateItemRequest(connectorId, parametersMap);
+ Call createItemRequestCall = clientWithEncryption.service().createItem(createItemRequest);
+ Response itemRequestResponse = createItemRequestCall.execute();
+ assertSuccessful(itemRequestResponse, clientWithEncryption);
+ ItemResponse itemResponse1 = itemRequestResponse.body();
+
+ assertNotNull(itemResponse1);
+ assertEquals(itemResponse1.getConnector().getId(), connectorId);
+
+ this.getItemsIdCreated().add(itemResponse1.getId());
+ }
}
diff --git a/src/test/java/ai/pluggy/client/integration/GetAccountTest.java b/src/test/java/ai/pluggy/client/integration/GetAccountTest.java
index 0835c10..63232b7 100644
--- a/src/test/java/ai/pluggy/client/integration/GetAccountTest.java
+++ b/src/test/java/ai/pluggy/client/integration/GetAccountTest.java
@@ -12,11 +12,9 @@
import ai.pluggy.client.response.ErrorResponse;
import java.io.IOException;
import lombok.SneakyThrows;
-import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.Test;
import retrofit2.Response;
-@Log4j2
public class GetAccountTest extends BaseApiIntegrationTest {
@Test
diff --git a/src/test/java/ai/pluggy/client/integration/GetAccountsTest.java b/src/test/java/ai/pluggy/client/integration/GetAccountsTest.java
index 59ebcd7..7b4eb32 100644
--- a/src/test/java/ai/pluggy/client/integration/GetAccountsTest.java
+++ b/src/test/java/ai/pluggy/client/integration/GetAccountsTest.java
@@ -14,12 +14,10 @@
import java.util.List;
import java.util.Objects;
import lombok.SneakyThrows;
-import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import retrofit2.Response;
-@Log4j2
public class GetAccountsTest extends BaseApiIntegrationTest {
private ItemResponse pluggyBankExecution;
diff --git a/src/test/java/ai/pluggy/client/integration/GetConnectorsTest.java b/src/test/java/ai/pluggy/client/integration/GetConnectorsTest.java
index 68af500..ff79763 100644
--- a/src/test/java/ai/pluggy/client/integration/GetConnectorsTest.java
+++ b/src/test/java/ai/pluggy/client/integration/GetConnectorsTest.java
@@ -7,7 +7,6 @@
import ai.pluggy.client.response.ConnectorType;
import ai.pluggy.client.response.ConnectorsResponse;
import java.io.IOException;
-import java.util.Arrays;
import java.util.Collections;
import org.junit.jupiter.api.Test;
diff --git a/src/test/java/ai/pluggy/client/unit/ClientParseErrorTest.java b/src/test/java/ai/pluggy/client/unit/ClientParseErrorTest.java
index a27646b..92be7bc 100644
--- a/src/test/java/ai/pluggy/client/unit/ClientParseErrorTest.java
+++ b/src/test/java/ai/pluggy/client/unit/ClientParseErrorTest.java
@@ -2,12 +2,10 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import ai.pluggy.client.PluggyClient;
import ai.pluggy.client.response.ErrorResponse;
-import com.google.gson.JsonParseException;
import java.io.IOException;
import okhttp3.MediaType;
import okhttp3.ResponseBody;