Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Can not decrypt saleToPOISecuredResponse #1430

Open
TrungPQ-konbini opened this issue Feb 10, 2025 · 6 comments
Open

Can not decrypt saleToPOISecuredResponse #1430

TrungPQ-konbini opened this issue Feb 10, 2025 · 6 comments

Comments

@TrungPQ-konbini
Copy link

TrungPQ-konbini commented Feb 10, 2025

Hi,

I'm using Terminal API through LAN communication to communicate with the terminal.

My code fails on this particular line 76 in TerminalLocalAPI.java because nexoBlob null
String jsonDecryptedResponse = nexoCrypto.decrypt(saleToPOISecuredResponse);

The error message (HTTP Exception) I receive is:

"Response":{"AdditionalResponse":"errors=At%20SaleToPOIRequest%2c%20field%20PaymentRequest%3a%20Missing&note=Direct%20API%20REQUIRES%20payload%20crypto%20on%20live%2c%20cleartext%20allowed%20for%20testing%20only","ErrorCondition":"MessageFormat","Result":"Failure"},

please let me know why and how i can fix this error

@gcatanese
Copy link
Contributor

Hi, yes nexoBlob shouldn't be empty or null. It might be an issue with the setup of the encryption.

Did you check the setup and recommendations in our Docs page?

Can you also provide more detail about how you encrypt and send the request? Thank you.

@Kwok-he-Chu
Copy link
Member

Have you set up a shared key in the customer area according to: https://docs.adyen.com/point-of-sale/design-your-integration/choose-your-architecture/local/#set-up-shared-key

  1. Ensure that your terminal is updated to the latest version (after setting up the shared key in the Customer Area, the will only visible after updating your terminal)
  2. Check the setup (encrypytion) on the docs page here
  3. If the two options above didn't help, could you share the request object that you're sending to the terminal?

@TrungPQ-konbini
Copy link
Author

@gcatanese @Kwok-he-Chu i'm using Protect with a library with https://github.com/Adyen/adyen-java-api-library, i dont use Protect with your own code. i tried with this code but it still NULL

// Step 1: Import the required classes
import com.adyen.service.TerminalLocalAPI;
import com.adyen.model.nexo.*;
import com.adyen.model.terminal.*;
import javax.net.ssl.SSLContext;

// Step 2: Add your Certificate Path and Local Endpoint to the config path.
Client client = new Client();
client.getConfig().setTerminalApiLocalEndpoint("The IP of your terminal (eg https://192.168.47.169)");
client.getConfig().setEnvironment(Environment.TEST);
config.setSSLContext(createTrustSSLContext()); // Trust all certificates for testing only
client.setConfig(config);

// Step 3: Create an SSL context that accepts all certificates (Use in TEST only).
SSLContext createTrustSSLContext() throws Exception {
    TrustManager[] trustAllCerts = new TrustManager[]{
            new X509TrustManager() {
                java.security.cert.X509Certificate[] getAcceptedIssuers() { return null; }
                checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) {}
                checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) {}
            }
    };
    SSLContext sc = SSLContext.getInstance("SSL");
    sc.init(null, trustAllCerts, new java.security.SecureRandom());
    return sc;
}

// Step 4: Construct a TerminalAPIRequest object
Gson gson = new GsonBuilder().create();
TerminalAPIRequest terminalAPIPaymentRequest = new TerminalAPIRequest();

// Step 5: Make the request
TerminalAPIResponse terminalAPIResponse = terminalLocalAPI.request(terminalAPIRequest);

Copy link

github-actions bot commented Mar 7, 2025

This issue has been automatically marked as stale due to inactivity and will be closed in 7 days if no further activity occurs.

@github-actions github-actions bot added the stale label Mar 7, 2025
@thanili
Copy link

thanili commented Mar 7, 2025

Any information on that? I have the exact same problem. In short:

I have followed the documentation to implement in person payments using local communications.

I am protecting my code using Adyen's API library (33.0.0) for java:
https://docs.adyen.com/point-of-sale/design-your-integration/choose-your-architecture/local/protect-with-library/
https://github.com/Adyen/adyen-java-api-library?tab=readme-ov-file#using-the-local-terminal-api-integration-without-encryption-only-on-test

I have gone through the relevant steps in the documentation so I have installed Adyen's certificates and setup a shared key and I am at the point where I want to add code to protect local communications.

Here is how I create a service that returns a TerminalLocalApi object to be used in my requests:

@Service
@CommonsLog
public class TerminalLocalApiService {
    private AdyenConfiguration adyenConfiguration;
    private Client client;
    private TerminalLocalAPI terminalLocalAPI;

    @Autowired
    public TerminalLocalApiService(AdyenConfiguration adyenConfiguration) {
        this.adyenConfiguration = adyenConfiguration;

        try {
            String host = "https://" + "192.168.221.91";
            Certificate adyenRootCertificate = loadAdyenRootCertificate();

            // Create a KeyStore for the terminal certificate
            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            keyStore.load(null, null);
            keyStore.setCertificateEntry("adyenRootCertificate", adyenRootCertificate);

            // Create a TrustManagerFactory that trusts the CAs in our KeyStore
            String algorithm = TrustManagerFactory.getDefaultAlgorithm();
            TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(algorithm);
            trustManagerFactory.init(keyStore);

            // Create an SSLContext with the desired protocol that uses our TrustManagers
            SSLContext sslContext = SSLContext.getInstance("SSL");
            sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());

            // Configure a client for TerminalLocalAPI
            Config config = new Config();
            config.setEnvironment(Environment.TEST);
            config.setTerminalApiLocalEndpoint(host);
            config.setSSLContext(sslContext);
            config.setHostnameVerifier(new TerminalLocalAPIHostnameVerifier(Environment.TEST));
            
            client = new Client(config);

            client.setEnvironment(Environment.TEST);

            // Create your SecurityKey object used for encrypting the payload (keyIdentifier/passphrase you set up beforehand in CA)
            SecurityKey securityKey = new SecurityKey();
            securityKey.setKeyVersion(1);
            securityKey.setAdyenCryptoVersion(1);
            securityKey.setKeyIdentifier("mspos-test-key");
            securityKey.setPassphrase("mspospassphrase");

            terminalLocalAPI = new TerminalLocalAPI(client, securityKey);
        } catch (NexoCryptoException e) {
            throw new RuntimeException(e);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public Certificate loadAdyenRootCertificate() {
        try {
            KeyStore trustedRootCertificatesStore = KeyStore.getInstance("Windows-ROOT");
            trustedRootCertificatesStore.load(null, null);
            printKeystoreAliases(trustedRootCertificatesStore);

            // Specify the alias of the certificate you want to retrieve
            String certificateAlias = "Adyen Test Terminal Fleet Root"; // Replace this with your certificate's alias
            Certificate adyenRootCertificate = trustedRootCertificatesStore.getCertificate(certificateAlias);

            if (adyenRootCertificate != null) {
                log.info("Certificate successfully loaded: " + adyenRootCertificate);
                return adyenRootCertificate;
            } else {
                log.error("Certificate with alias " + certificateAlias + " not found!");
            }
        } catch (Exception e) {
            log.error("Error while loading the certificate.", e);
        }
        return null;
    }

I use this object to send requests directly to my (test) payment terminal:

public TerminalAPIResponse sendPaymentRequestSync(String serviceId, String poiId, String saleId, String currency, BigDecimal amount, BigDecimal tipAmount, AdyenTerminal terminal) {
        TerminalAPIRequest request = getPaymentRequest(serviceId, poiId, saleId, currency, amount, tipAmount);
        adyenLoggingHelper.logRequest(request);
        try {
            //terminalLocalAPIService.setTerminalIpAddress(terminal.getIp());
            var response = terminalLocalAPIService.getTerminalLocalApi().request(request);
            return response;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

However I am getting the exact same error that I can locate in Adyen's java library TerminalLocalAPI class (when debugging):

public class TerminalLocalAPI extends Service {
    private final LocalRequest localRequest = new LocalRequest(this);
    private final NexoCrypto nexoCrypto;
    private final Gson terminalApiGson;

    public TerminalLocalAPI(Client client, SecurityKey securityKey) throws NexoCryptoException {
        super(client);
        this.nexoCrypto = new NexoCrypto(securityKey);
        this.terminalApiGson = TerminalAPIGsonBuilder.create();
    }

    public TerminalAPIResponse request(TerminalAPIRequest terminalAPIRequest) throws Exception {
        String jsonRequest = this.terminalApiGson.toJson(terminalAPIRequest);
        SaleToPOISecuredMessage saleToPOISecuredRequest = this.nexoCrypto.encrypt(jsonRequest, terminalAPIRequest.getSaleToPOIRequest().getMessageHeader());
        TerminalAPISecuredRequest securedPaymentRequest = new TerminalAPISecuredRequest();
        securedPaymentRequest.setSaleToPOIRequest(saleToPOISecuredRequest);
        String jsonEncryptedRequest = this.terminalApiGson.toJson(securedPaymentRequest);
        String jsonResponse = this.localRequest.request(jsonEncryptedRequest);
        if (jsonResponse != null && !jsonResponse.isEmpty()) {
            TerminalAPISecuredResponse securedPaymentResponse = (TerminalAPISecuredResponse)this.terminalApiGson.fromJson(jsonResponse, (new TypeToken<TerminalAPISecuredResponse>() {
            }).getType());
            SaleToPOISecuredMessage saleToPOISecuredResponse = securedPaymentResponse.getSaleToPOIResponse();
            String jsonDecryptedResponse = this.nexoCrypto.decrypt(saleToPOISecuredResponse);
            return (TerminalAPIResponse)this.terminalApiGson.fromJson(jsonDecryptedResponse, (new TypeToken<TerminalAPIResponse>() {
            }).getType());
        } else {
            return null;
        }
    }
}

More specifically I can see that the jsonRequest is received correctly in the request method. For example:

{
    "SaleToPOIRequest": {
        "MessageHeader": {
            "MessageClass": "Service",
            "MessageCategory": "Payment",
            "MessageType": "Request",
            "ServiceID": "7RW7uqloFL",
            "SaleID": "PosStation1",
            "POIID": "V400cPlus-452903256"
        },
        "PaymentRequest": {
            "SaleData": {
                "SaleTransactionID": {
                    "TransactionID": "a3417448-3467-44c7-a127-043c33996c8d",
                    "TimeStamp": "2025-03-07T11:47:43.529+02:00"
                },
                "SaleToAcquirerData": "ewogICJhcHBsaWNhdGlvbkluZm8iOiB7CiAgICAiYWR5ZW5MaWJyYXJ5IjogewogICAgICAibmFtZSI6ICJhZHllbi1qYXZhLWFwaS1saWJyYXJ5IiwKICAgICAgInZlcnNpb24iOiAiMzMuMC4wIgogICAgfQogIH0KfQ=="
            },
            "PaymentTransaction": {
                "AmountsReq": {
                    "Currency": "EUR",
                    "RequestedAmount": 16.99
                }
            }
        }
    }
}

and it's encrypted in nexoblob (jsonEncryptedRequest):

{
    "SaleToPOIRequest": {
        "MessageHeader": {
            "MessageClass": "Service",
            "MessageCategory": "Payment",
            "MessageType": "Request",
            "ServiceID": "7RW7uqloFL",
            "SaleID": "PosStation1",
            "POIID": "V400cPlus-452903256"
        },
        "NexoBlob": "Qy5DNTUEXmwkiPbQLKQBvSkyBS7rTkc36cD94zScZdVRZ5Eh6qCmWT0h6Fa8fbvWzKCyf1wCEd+EdnFxN9USyWB2rhTgjiovG+NyoxGidZLLhsBl/KdXEbV+aO5R9Ks6ckk+thJaRJeFfSYaCKx7Ff4sSfRjjvtfi+urGJ5kMvApvUKfOMr60112K/ERJzj0iPK1RcAxF6z7DyhF+pb6gPWG2F35yuGPoFOCOKBFyOd/vzFhjGXnMYX2vEoU/3e6iHF+RmpLSW1e882G/HdCsSpdYZVwoY7ZSetf3nXlBaHEeKOLquKF9FxcVXJp00dMiWLsxBjPKbScOh92Ph18L14B6sJgkY1Hexh9KLoOZYijRvKOEG8kO1dGiv4mI97BCWQzDSD+wZ/Dre1HY0PpZ1ft6tJWKLqK93ExOR0tfARaQpGJfCjrAZg/8qeJjZpedLdlaptGIk+UOpkIroaawIBZIldk1wxR9NPyff75avAu6CJzczLOIUGk58ul1KwqX9+FXS4S3cXTcGih6whLLeBDLh0xZHoNAGT8rFBwQv0PDGKR2NNUevinYnEVZIDI/sfcyYm792SjYIYN/VT7aFR1DmipqhCqFJxS3pNN3Y4f/luiZkpJERK+/wIIkqrnJDwH+1gWC1auHL4GbyIypy7dmXnUs34xtpXlOFe6NJg8nRxjc0N7bkbmATEgp4+knl9j8zbpB7oiBTgfWQN2j6c4S4qVbN2SXM/4GcBGjNU/cWDggjWQB7lfOt3ulScxFLM5RGNrMFgg1YmOkr2/QSD2atyha9HkNnGECmPV4nglzsqzlWoR4rjIUmnysbSc",
        "SecurityTrailer": {
            "AdyenCryptoVersion": 1,
            "KeyIdentifier": "mspos-test-key",
            "KeyVersion": 1,
            "Nonce": "YApi6Xqkv7/2jHExRmjkiA==",
            "Hmac": "XooCPEBNLdSMuJcambl0RYXxt/5KwYcY5yY4Uzrc6RM="
        }
    }
}

Then I am getting a jsonResponse with an error:

{
    "SaleToPOIResponse": {
        "MessageHeader": {
            "MessageCategory": "Payment",
            "MessageClass": "Service",
            "MessageType": "Response",
            "POIID": "V400cPlus-452903256",
            "SaleID": ""
        },
        "PaymentResponse": {
            "POIData": {
                "POITransactionID": {
                    "TimeStamp": "2025-03-07T09:50:23.692Z",
                    "TransactionID": "ZyYK001741341023002"
                }
            },
            "Response": {
                "AdditionalResponse": "errors=At%20SaleToPOIRequest%2c%20field%20PaymentRequest%3a%20Missing&note=Direct%20API%20REQUIRES%20payload%20crypto%20on%20live%2c%20cleartext%20allowed%20for%20testing%20only",
                "ErrorCondition": "MessageFormat",
                "Result": "Failure"
            },
            "SaleData": {
                "SaleTransactionID": {
                    "TimeStamp": "2025-03-07T09:50:23.692Z",
                    "TransactionID": ""
                }
            }
        }
    }
}

The code in the library crashes because the jsonResponse is not valid (can not handle this case):

TerminalAPISecuredResponse securedPaymentResponse = (TerminalAPISecuredResponse)this.terminalApiGson.fromJson(jsonResponse, (new TypeToken<TerminalAPISecuredResponse>() {
            }).getType());
            SaleToPOISecuredMessage saleToPOISecuredResponse = securedPaymentResponse.getSaleToPOIResponse();
            String jsonDecryptedResponse = this.nexoCrypto.decrypt(saleToPOISecuredResponse);

since saleToPOISecuredResponse.nexoBlob is null.

When I was trying to test without encryption I was also getting the same error. Any kind of insights about that?

@thanili
Copy link

thanili commented Mar 7, 2025

Actually I have found what was the issue in my case.

The latest update in the terminal had failed, so it did not have the encryption key details. I updated the terminal and now I can receive the request in the terminal and successfully pay.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants