Skip to content

Commit 8bde7be

Browse files
feat(PluggyClient): add credentials encryption for POST/PATCH to /items and add rsaPublicKey method. (#45)
* refactor: clean up unused code. * chore(GetAccountsTest): remove unneeded dependency. * feat(PluggyClient) * chore: bump minor version. * fix(EncryptedParametersInterceptor): re-throw error.
1 parent 5e4f386 commit 8bde7be

File tree

15 files changed

+172
-42
lines changed

15 files changed

+172
-42
lines changed

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
<groupId>ai.pluggy</groupId>
77
<artifactId>pluggy-java</artifactId>
8-
<version>0.15.1</version>
8+
<version>0.16.0</version>
99

1010
<packaging>jar</packaging>
1111

src/main/java/ai/pluggy/client/PluggyApiService.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
import retrofit2.http.QueryMap;
3636

3737
public interface PluggyApiService {
38-
3938
@GET("/connectors")
4039
Call<ConnectorsResponse> getConnectors();
4140

src/main/java/ai/pluggy/client/PluggyClient.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import ai.pluggy.client.auth.ApiKeyAuthInterceptor;
77
import ai.pluggy.client.auth.AuthenticationHelper;
8+
import ai.pluggy.client.auth.EncryptedParametersInterceptor;
89
import ai.pluggy.client.response.ErrorResponse;
910
import ai.pluggy.exception.PluggyException;
1011
import ai.pluggy.utils.Utils;
@@ -28,14 +29,11 @@
2829
import retrofit2.converter.gson.GsonConverterFactory;
2930

3031
public final class PluggyClient {
31-
3232
public static final int ONE_MIB_BYTES = 1024 * 1024;
3333
public static String AUTH_URL_PATH = "/auth";
3434
private String baseUrl;
35-
3635
private String clientId;
3736
private String clientSecret;
38-
3937
private OkHttpClient httpClient;
4038
private PluggyApiService service;
4139

@@ -156,6 +154,7 @@ public static class PluggyClientBuilder {
156154
private String clientId;
157155
private String clientSecret;
158156
private String baseUrl;
157+
private String rsaPublicKey;
159158
private Builder okHttpClientBuilder;
160159
private boolean disableDefaultAuthInterceptor = false;
161160

@@ -180,6 +179,11 @@ public PluggyClientBuilder baseUrl(String baseUrl) {
180179
return this;
181180
}
182181

182+
public PluggyClientBuilder rsaPublicKey(String rsaPublicKey) {
183+
this.rsaPublicKey = rsaPublicKey;
184+
return this;
185+
}
186+
183187
/**
184188
* Opt-out from provided default ApiKeyAuthInterceptor, which takes care of apiKey
185189
* authorization, by requesting a new apiKey token when it's not set, or by reactively
@@ -211,6 +215,10 @@ private OkHttpClient buildOkHttpClient(String baseUrl) {
211215
.addInterceptor(new ApiKeyAuthInterceptor(authUrlPath, clientId, clientSecret));
212216
}
213217

218+
if (this.rsaPublicKey != null) {
219+
this.okHttpClientBuilder.addInterceptor(new EncryptedParametersInterceptor(this.rsaPublicKey));
220+
}
221+
214222
return okHttpClientBuilder.build();
215223
}
216224

@@ -278,7 +286,7 @@ public String authenticate() throws IOException, PluggyException {
278286
.post(body)
279287
.addHeader("content-type", "application/json")
280288
.addHeader("cache-control", "no-cache")
281-
.addHeader("User-Agent", "PluggyJava/0.15.1")
289+
.addHeader("User-Agent", "PluggyJava/0.16.0")
282290
.build();
283291

284292
String apiKey;

src/main/java/ai/pluggy/client/auth/ApiKeyAuthInterceptor.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import static ai.pluggy.utils.Asserts.assertNotNull;
44

5-
import ai.pluggy.utils.Utils;
65
import com.google.gson.Gson;
76

87
import java.io.IOException;
@@ -116,7 +115,7 @@ private Request requestWithAuth(Request originalRequest, String apiKey) {
116115
return originalRequest.newBuilder()
117116
.header(X_API_KEY_HEADER, apiKey)
118117
// TOOD: add dynamic version
119-
.header("User-Agent", "PluggyJava/0.15.1")
118+
.header("User-Agent", "PluggyJava/0.16.0")
120119
.build();
121120
}
122121

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package ai.pluggy.client.auth;
2+
3+
import static ai.pluggy.utils.Asserts.assertNotNull;
4+
5+
import java.io.IOException;
6+
import java.security.InvalidKeyException;
7+
import java.security.KeyFactory;
8+
import java.security.NoSuchAlgorithmException;
9+
import java.security.PublicKey;
10+
import java.security.spec.InvalidKeySpecException;
11+
import java.security.spec.X509EncodedKeySpec;
12+
13+
import okio.Buffer;
14+
import java.util.Arrays;
15+
import java.util.Base64;
16+
17+
import javax.crypto.BadPaddingException;
18+
import javax.crypto.Cipher;
19+
import javax.crypto.IllegalBlockSizeException;
20+
import javax.crypto.NoSuchPaddingException;
21+
22+
import org.jetbrains.annotations.NotNull;
23+
24+
import com.google.gson.Gson;
25+
import com.google.gson.JsonObject;
26+
27+
import okhttp3.Interceptor;
28+
import okhttp3.Request;
29+
import okhttp3.RequestBody;
30+
import okhttp3.Response;
31+
32+
public class EncryptedParametersInterceptor implements Interceptor {
33+
private String rsaPublicKey;
34+
private String path = "/items";
35+
private String[] methods = { "PATCH", "POST" };
36+
37+
public EncryptedParametersInterceptor(String rsaPublicKey) {
38+
assertNotNull(rsaPublicKey, rsaPublicKey);
39+
this.rsaPublicKey = rsaPublicKey;
40+
}
41+
42+
public Response intercept(@NotNull Chain chain) throws IOException {
43+
Request originalRequest = chain.request();
44+
String method = originalRequest.method();
45+
RequestBody originalBody = originalRequest.body();
46+
47+
if (originalBody == null) {
48+
return chain.proceed(originalRequest);
49+
}
50+
51+
JsonObject jsonBody = this.transformBodyToJsonObject(originalBody);
52+
53+
if (!Arrays.asList(methods).contains(method)
54+
|| !originalRequest.url().encodedPath().contains(path) && !jsonBody.has("parameters")) {
55+
return chain.proceed(originalRequest);
56+
}
57+
58+
String parameters = jsonBody.get("parameters").toString();
59+
60+
String encryptedParameters = encryptParameters(parameters);
61+
62+
jsonBody.addProperty("parameters", encryptedParameters);
63+
64+
// create new request with new body and same headers/params
65+
Request newRequest = originalRequest.newBuilder()
66+
.method(method, RequestBody.create(jsonBody.toString(), originalBody.contentType()))
67+
.build();
68+
69+
return chain.proceed(newRequest);
70+
}
71+
72+
private String encryptParameters(String parameters) throws IOException {
73+
String publicKeyPEM = this.rsaPublicKey
74+
.replace("-----BEGIN PUBLIC KEY-----", "")
75+
.replace("-----END PUBLIC KEY-----", "")
76+
.replaceAll("\\s+", "");
77+
78+
byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyPEM);
79+
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKeyBytes);
80+
81+
try {
82+
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
83+
PublicKey publicKey = keyFactory.generatePublic(keySpec);
84+
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPPadding");
85+
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
86+
byte[] encryptedPayloadBytes = cipher.doFinal(parameters.getBytes());
87+
String encryptedPayload = Base64.getEncoder().encodeToString(encryptedPayloadBytes);
88+
return encryptedPayload;
89+
90+
} catch (NoSuchAlgorithmException | InvalidKeySpecException | NoSuchPaddingException | InvalidKeyException
91+
| BadPaddingException | IllegalBlockSizeException error) {
92+
throw new IOException("Error encrypting parameters", error);
93+
}
94+
}
95+
96+
private JsonObject transformBodyToJsonObject(RequestBody body) throws IOException {
97+
Buffer buffer = new Buffer();
98+
try {
99+
body.writeTo(buffer);
100+
} catch (IOException error) {
101+
throw error;
102+
}
103+
String bodyString = buffer.readUtf8();
104+
105+
JsonObject jsonBody = new Gson().fromJson(bodyString, JsonObject.class);
106+
107+
return jsonBody;
108+
}
109+
}

src/main/java/ai/pluggy/client/request/CreateItemRequest.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
@Value
77
@AllArgsConstructor
88
public class CreateItemRequest {
9-
109
Integer connectorId;
1110
ParametersMap parameters;
1211
String webhookUrl;

src/main/java/ai/pluggy/client/response/Account.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
package ai.pluggy.client.response;
22

3-
4-
import java.util.ArrayList;
5-
import java.util.List;
63
import lombok.Data;
74

85
@Data

src/main/java/ai/pluggy/client/response/WebhookEventType.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package ai.pluggy.client.response;
22

33
import com.google.gson.annotations.SerializedName;
4-
import lombok.AllArgsConstructor;
5-
import lombok.Getter;
64

75
public enum WebhookEventType {
86
@SerializedName("item/created")

src/main/java/ai/pluggy/exception/PluggyException.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package ai.pluggy.exception;
22

3-
import java.io.IOException;
43
import okhttp3.Response;
54

65
public class PluggyException extends Exception {

src/test/java/ai/pluggy/client/integration/BaseApiIntegrationTest.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@
1111
import org.junit.platform.commons.util.StringUtils;
1212

1313
class BaseApiIntegrationTest {
14-
1514
static final String CLIENT_ID = System.getenv("PLUGGY_CLIENT_ID");
1615
static final String CLIENT_SECRET = System.getenv("PLUGGY_CLIENT_SECRET");
1716
static final String TEST_BASE_URL = System.getenv("PLUGGY_BASE_URL");
17+
static final String RSA_PUBLIC_KEY = System.getenv("PLUGGY_RSA_PUBLIC_KEY");
1818

1919
PluggyClient client;
20-
20+
2121
protected List<String> itemsIdCreated = new ArrayList<>();
2222

2323
public List<String> getItemsIdCreated() {
@@ -28,9 +28,9 @@ public List<String> getItemsIdCreated() {
2828
void setUp() {
2929
checkEnvErrors();
3030
client = PluggyClient.builder()
31-
.baseUrl(TEST_BASE_URL)
32-
.clientIdAndSecret(CLIENT_ID, CLIENT_SECRET)
33-
.build();
31+
.baseUrl(TEST_BASE_URL)
32+
.clientIdAndSecret(CLIENT_ID, CLIENT_SECRET)
33+
.build();
3434
}
3535

3636
protected void checkEnvErrors() {
@@ -46,12 +46,12 @@ protected void checkEnvErrors() {
4646
}
4747
if (missingEnvVars.size() > 0) {
4848
String envVarsListString = missingEnvVars.stream()
49-
.map(varName -> "'" + varName + "'")
50-
.collect(Collectors.joining(", "));
49+
.map(varName -> "'" + varName + "'")
50+
.collect(Collectors.joining(", "));
5151
throw new IllegalStateException("Must define " + envVarsListString + " env var(s)!");
5252
}
5353
}
54-
54+
5555
@SneakyThrows
5656
@AfterEach
5757
protected void clearItems() {

0 commit comments

Comments
 (0)