Skip to content

Commit 2f39f94

Browse files
committed
Move encryption capabilities to different service
1 parent 7388f5c commit 2f39f94

9 files changed

Lines changed: 330 additions & 335 deletions

File tree

src/main/java/com/adyen/model/clouddevice/security/NexoDerivedKey.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,27 +46,27 @@ public NexoDerivedKey() {
4646
}
4747

4848
public byte[] getHmacKey() {
49-
return hmacKey;
49+
return Arrays.copyOf(hmacKey, hmacKey.length);
5050
}
5151

5252
public void setHmacKey(byte[] hmacKey) {
53-
this.hmacKey = hmacKey;
53+
this.hmacKey = Arrays.copyOf(hmacKey, hmacKey.length);
5454
}
5555

5656
public byte[] getCipherKey() {
57-
return cipherKey;
57+
return Arrays.copyOf(cipherKey, cipherKey.length);
5858
}
5959

6060
public void setCipherKey(byte[] cipherKey) {
61-
this.cipherKey = cipherKey;
61+
this.cipherKey = Arrays.copyOf(cipherKey, cipherKey.length);
6262
}
6363

6464
public byte[] getIv() {
65-
return iv;
65+
return Arrays.copyOf(iv, iv.length);
6666
}
6767

6868
public void setIv(byte[] iv) {
69-
this.iv = iv;
69+
this.iv = Arrays.copyOf(iv, iv.length);
7070
}
7171

7272
@Override

src/main/java/com/adyen/security/clouddevice/EncryptionCredentialDetails.java

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,20 @@
11
package com.adyen.security.clouddevice;
22

3-
import com.adyen.model.tapi.JSON;
4-
import com.fasterxml.jackson.annotation.JsonProperty;
5-
import com.fasterxml.jackson.core.JsonProcessingException;
63
import java.util.Objects;
74

85
/** Details of the encryption credential used for encrypting the request payload (nexoBlob) */
96
public class EncryptionCredentialDetails {
107

118
/** The passphrase used to derive the encryption key. */
12-
@JsonProperty("Passphrase")
139
private String passphrase;
1410

1511
/** The unique identifier of the key. */
16-
@JsonProperty("KeyIdentifier")
1712
private String keyIdentifier;
1813

1914
/** The version of the key. */
20-
@JsonProperty("KeyVersion")
2115
private Integer keyVersion;
2216

2317
/** The version of the Adyen-specific crypto implementation. */
24-
@JsonProperty("AdyenCryptoVersion")
2518
private Integer adyenCryptoVersion;
2619

2720
public String getPassphrase() {
@@ -76,28 +69,6 @@ public EncryptionCredentialDetails adyenCryptoVersion(Integer adyenCryptoVersion
7669
return this;
7770
}
7871

79-
/**
80-
* Create an instance of EncryptionCredentialDetails given an JSON string
81-
*
82-
* @param jsonString JSON string
83-
* @return An instance of EncryptionCredentialDetails
84-
* @throws JsonProcessingException if the JSON string is invalid with respect to
85-
* EncryptionCredentialDetails
86-
*/
87-
public static EncryptionCredentialDetails fromJson(String jsonString)
88-
throws JsonProcessingException {
89-
return JSON.getMapper().readValue(jsonString, EncryptionCredentialDetails.class);
90-
}
91-
92-
/**
93-
* Convert an instance of EncryptionCredentialDetails to an JSON string
94-
*
95-
* @return JSON string
96-
*/
97-
public String toJson() throws JsonProcessingException {
98-
return JSON.getMapper().writeValueAsString(this);
99-
}
100-
10172
@Override
10273
public boolean equals(Object o) {
10374
if (this == o) {

src/main/java/com/adyen/security/clouddevice/NexoCryptoPrimitives.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ static byte[] generateRandomIvNonce() {
8686
SecureRandom secureRandom;
8787
try {
8888
secureRandom = SecureRandom.getInstance("NativePRNGNonBlocking");
89-
} catch (Exception ignored) {
89+
} catch (NoSuchAlgorithmException ignored) {
9090
secureRandom = new SecureRandom();
9191
}
9292
secureRandom.nextBytes(ivNonce);

src/main/java/com/adyen/security/clouddevice/NexoDerivedKeyGenerator.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
package com.adyen.security.clouddevice;
2-
31
/*
42
* ######
53
* ######
@@ -20,6 +18,7 @@
2018
* This file is open source and available under the MIT license.
2119
* See the LICENSE file for more info.
2220
*/
21+
package com.adyen.security.clouddevice;
2322

2423
import static com.adyen.model.clouddevice.security.NexoDerivedKey.NEXO_CIPHER_KEY_LENGTH;
2524
import static com.adyen.model.clouddevice.security.NexoDerivedKey.NEXO_HMAC_KEY_LENGTH;
@@ -41,6 +40,7 @@ private NexoDerivedKeyGenerator() {}
4140
static NexoDerivedKey deriveKeyMaterial(String passphrase)
4241
throws NoSuchAlgorithmException, InvalidKeySpecException {
4342
byte[] salt = "AdyenNexoV1Salt".getBytes(StandardCharsets.UTF_8);
43+
// Iteration count is fixed at 4000 as mandated by the Nexo protocol specification (AdyenNexoV1)
4444
int iterations = 4000;
4545

4646
PBEKeySpec spec =

src/main/java/com/adyen/security/clouddevice/NexoSecurityException.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,4 @@ public NexoSecurityException(String message, Throwable cause) {
4444
public NexoSecurityException(Throwable cause) {
4545
super(cause);
4646
}
47-
48-
@Override
49-
public String toString() {
50-
return "NexoSecurityException{message=" + getMessage() + '}';
51-
}
5247
}

src/main/java/com/adyen/service/clouddevice/CloudDeviceApi.java

Lines changed: 1 addition & 190 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,13 @@
55
import com.adyen.constants.ApiConstants;
66
import com.adyen.model.clouddevice.*;
77
import com.adyen.model.clouddevice.CloudDeviceApiAsyncResponse;
8-
import com.adyen.security.clouddevice.EncryptionCredentialDetails;
9-
import com.adyen.security.clouddevice.NexoSecurityException;
10-
import com.adyen.security.clouddevice.NexoSecurityManager;
118
import com.adyen.service.exception.ApiException;
129
import com.adyen.service.resource.Resource;
13-
import com.fasterxml.jackson.databind.JsonNode;
14-
import com.fasterxml.jackson.databind.ObjectMapper;
1510
import java.io.IOException;
1611
import java.util.HashMap;
1712
import java.util.Map;
1813

14+
/** Service of the Cloud Device API */
1915
public class CloudDeviceApi extends Service {
2016

2117
public static final String API_VERSION = "1";
@@ -216,189 +212,4 @@ public DeviceStatusResponse getDeviceStatus(String merchantAccount, String devic
216212

217213
return DeviceStatusResponse.fromJson(response);
218214
}
219-
220-
/**
221-
* Send a synchronous encrypted request.
222-
*
223-
* @param merchantAccount The unique identifier of the merchant account
224-
* @param deviceId The unique identifier of the device that you send this request to (must match
225-
* POIID in the MessageHeader).
226-
* @param cloudDeviceApiRequest The request to send.
227-
* @return instance of CloudDeviceApiResponse
228-
* @throws ApiException when an error occurs
229-
* @throws IOException when an I/O error occurs
230-
* @throws NexoSecurityException when encryption or decryption fails
231-
*/
232-
public CloudDeviceApiResponse syncEncrypted(
233-
String merchantAccount,
234-
String deviceId,
235-
CloudDeviceApiRequest cloudDeviceApiRequest,
236-
EncryptionCredentialDetails encryptionCredentialDetails)
237-
throws ApiException, IOException, NexoSecurityException {
238-
239-
NexoSecurityManager nexoSecurityManager = new NexoSecurityManager(encryptionCredentialDetails);
240-
241-
// Add path params
242-
Map<String, String> pathParams = new HashMap<>();
243-
244-
if (merchantAccount == null) {
245-
throw new IllegalArgumentException("Please provide the merchantAccount path parameter");
246-
}
247-
pathParams.put("merchantAccount", merchantAccount);
248-
249-
if (deviceId == null) {
250-
throw new IllegalArgumentException("Please provide the deviceId path parameter");
251-
}
252-
pathParams.put("deviceId", deviceId);
253-
254-
if (cloudDeviceApiRequest.getSaleToPOIRequest() == null
255-
|| cloudDeviceApiRequest.getSaleToPOIRequest().getMessageHeader() == null) {
256-
throw new IllegalArgumentException(
257-
"cloudDeviceApiRequest must contain a SaleToPOIRequest with a MessageHeader");
258-
}
259-
cloudDeviceApiRequest.getSaleToPOIRequest().getMessageHeader().setPOIID(deviceId);
260-
261-
// encrypt payload
262-
SaleToPOISecuredMessage saleToPOISecuredRequest =
263-
nexoSecurityManager.encrypt(
264-
cloudDeviceApiRequest.toJson(),
265-
cloudDeviceApiRequest.getSaleToPOIRequest().getMessageHeader());
266-
267-
CloudDeviceApiSecuredRequest cloudDeviceApiSecuredRequest = new CloudDeviceApiSecuredRequest();
268-
cloudDeviceApiSecuredRequest.setSaleToPOIRequest(saleToPOISecuredRequest);
269-
270-
String encryptedJson = cloudDeviceApiSecuredRequest.toJson();
271-
272-
// perform API call
273-
Resource resource =
274-
new Resource(
275-
this, this.baseURL + "/merchants/{merchantAccount}/devices/{deviceId}/sync", null);
276-
String response =
277-
resource.request(encryptedJson, null, ApiConstants.HttpMethod.POST, pathParams);
278-
279-
CloudDeviceApiSecuredResponse cloudDeviceApiSecuredResponse =
280-
CloudDeviceApiSecuredResponse.fromJson(response);
281-
282-
String jsonDecryptedResponse =
283-
nexoSecurityManager.decrypt(cloudDeviceApiSecuredResponse.getSaleToPOIResponse());
284-
285-
return CloudDeviceApiResponse.fromJson(jsonDecryptedResponse);
286-
}
287-
288-
/**
289-
* Send an asynchronous encrypted request.
290-
*
291-
* @param merchantAccount The unique identifier of the merchant account
292-
* @param deviceId The unique identifier of the device that you send this request to (must match
293-
* POIID in the MessageHeader).
294-
* @param cloudDeviceApiRequest The request to send.
295-
* @return "ok" on success
296-
* @throws ApiException when an error occurs
297-
* @throws IOException when an I/O error occurs
298-
* @throws NexoSecurityException when encryption or decryption fails
299-
*/
300-
public String asyncEncrypted(
301-
String merchantAccount,
302-
String deviceId,
303-
CloudDeviceApiRequest cloudDeviceApiRequest,
304-
EncryptionCredentialDetails encryptionCredentialDetails)
305-
throws ApiException, IOException, NexoSecurityException {
306-
307-
NexoSecurityManager nexoSecurityManager = new NexoSecurityManager(encryptionCredentialDetails);
308-
309-
// Add path params
310-
Map<String, String> pathParams = new HashMap<>();
311-
312-
if (merchantAccount == null) {
313-
throw new IllegalArgumentException("Please provide the merchantAccount path parameter");
314-
}
315-
pathParams.put("merchantAccount", merchantAccount);
316-
317-
if (deviceId == null) {
318-
throw new IllegalArgumentException("Please provide the deviceId path parameter");
319-
}
320-
pathParams.put("deviceId", deviceId);
321-
322-
if (cloudDeviceApiRequest.getSaleToPOIRequest() == null
323-
|| cloudDeviceApiRequest.getSaleToPOIRequest().getMessageHeader() == null) {
324-
throw new IllegalArgumentException(
325-
"cloudDeviceApiRequest must contain a SaleToPOIRequest with a MessageHeader");
326-
}
327-
cloudDeviceApiRequest.getSaleToPOIRequest().getMessageHeader().setPOIID(deviceId);
328-
329-
// encrypt payload
330-
SaleToPOISecuredMessage saleToPOISecuredRequest =
331-
nexoSecurityManager.encrypt(
332-
cloudDeviceApiRequest.toJson(),
333-
cloudDeviceApiRequest.getSaleToPOIRequest().getMessageHeader());
334-
335-
CloudDeviceApiSecuredRequest cloudDeviceApiSecuredRequest = new CloudDeviceApiSecuredRequest();
336-
cloudDeviceApiSecuredRequest.setSaleToPOIRequest(saleToPOISecuredRequest);
337-
338-
String encryptedJson = cloudDeviceApiSecuredRequest.toJson();
339-
340-
// perform API call
341-
Resource resource =
342-
new Resource(
343-
this, this.baseURL + "/merchants/{merchantAccount}/devices/{deviceId}/async", null);
344-
345-
// async responses are decrypted
346-
String response =
347-
resource.request(encryptedJson, null, ApiConstants.HttpMethod.POST, pathParams);
348-
349-
return response;
350-
}
351-
352-
/**
353-
* Decrypt an event notification
354-
*
355-
* @param payload Event notification in JSON string format: it can be SaleToPOIResponse (async
356-
* response) or SaleToPOIRequest (event notification)
357-
* @param encryptionCredentialDetails The details of the encryption credential used for decrypting
358-
* the payload (nexoBlob)
359-
* @return the decrypted payload
360-
* @throws NexoSecurityException when decryption fails
361-
*/
362-
public String decryptNotification(
363-
String payload, EncryptionCredentialDetails encryptionCredentialDetails)
364-
throws NexoSecurityException {
365-
366-
try {
367-
NexoSecurityManager nexoSecurityManager =
368-
new NexoSecurityManager(encryptionCredentialDetails);
369-
370-
ObjectMapper objectMapper = new ObjectMapper();
371-
JsonNode jsonNode;
372-
373-
try {
374-
jsonNode = objectMapper.readTree(payload);
375-
} catch (Exception e) {
376-
throw new NexoSecurityException("Invalid payload");
377-
}
378-
379-
String decryptedMessage;
380-
if (jsonNode.has("SaleToPOIResponse")) {
381-
// async response received
382-
CloudDeviceApiSecuredResponse cloudDeviceApiSecuredResponse =
383-
CloudDeviceApiSecuredResponse.fromJson(payload);
384-
decryptedMessage =
385-
nexoSecurityManager.decrypt(cloudDeviceApiSecuredResponse.getSaleToPOIResponse());
386-
} else if (jsonNode.has("SaleToPOIRequest")) {
387-
CloudDeviceApiSecuredRequest cloudDeviceApiSecuredRequest =
388-
CloudDeviceApiSecuredRequest.fromJson(payload);
389-
decryptedMessage =
390-
nexoSecurityManager.decrypt(cloudDeviceApiSecuredRequest.getSaleToPOIRequest());
391-
} else {
392-
throw new NexoSecurityException(
393-
"Unexpected payload without SaleToPOIResponse or SaleToPOIRequest");
394-
}
395-
396-
return decryptedMessage;
397-
398-
} catch (NexoSecurityException e) {
399-
throw e;
400-
} catch (Exception e) {
401-
throw new NexoSecurityException(e.getMessage(), e);
402-
}
403-
}
404215
}

0 commit comments

Comments
 (0)