Search

Are you looking for test card numbers?

Would you like to contact support?

Point-of-sale icon

Protect local communications

Validate and encrypt local communications between your cash register and terminal.

If your integration uses local communications, you need to take some additional steps to help protect your integration against man-in-the-middle attacks, eavesdropping, and tampering.

To protect the communications between your cash register and terminal, you need to:

Validate terminal certificate

Every Adyen terminal comes pre-installed with a certificate signed by Adyen. To confirm that your cash register is communicating directly with an Adyen terminal, you need to:

  1. Install Adyen's root certificate to your trust store.
  2. Authenticate the connection between the cash register and terminal, by verifying the certificate on the terminal.

Step 1: Install certificate

To install Adyen's public root certificate in your trust store:

  1. Download Adyen's public root certificates to your cash register:

    You can also convert these certificates to formats such as .crt, .cer, or .p12 with openssl, if you need to import them to your system.

  2. Verify that the SHA-256 signature on the certificates match the following:

    • TEST terminal certificate: 3A 33 C3 34 C3 0F 69 46 E9 75 4B 6B B1 67 2B 54 6F BA A9 66 FB 6A 4B 58 AA 4E 3A BE 80 A7 EC BE
    • LIVE terminal certificate: 06 D4 86 41 95 4B 95 7D 7A F5 F5 E4 5A 58 D8 61 DB 0D E3 CC ED BB 98 36 60 BB 01 6C E6 14 2D A1

    In a Windows environment the SHA-256 fingerprint may not be readily available through code or MMC. In this case verify the SHA-1 signature on the certificates instead:

    • TEST terminal certificate: D5 02 7F A8 B3 93 96 DB 2A 4F B1 86 EF 61 E4 A4 40 A7 30 51
    • LIVE terminal certificate: 62 61 0D 88 27 8E 95 B7 F8 57 9A 9B 5E 07 85 D7 72 87 66 42

  3. Install the certificates in your cash register's trust store.

    Follow your vendor's instructions for adding root certificates to either the user or system trust store.
    At the user level, it is easier to import a certificate, and is generally preferred over lower levels.
    For example, Microsoft's documentation for adding certificates using the MMC snap-in, or importing certificates with the PowerShell.
    For iOS see: iOS certificate handling.

After you have installed the certificates in your trust store, verify the certificate from the terminal on your cash register.

Step 2: Verify certificate

When your cash register connects to the terminal, it should:

  1. Verify that the certificate on the terminal is signed by the trusted root certificate that you installed. This step automatically includes checking of the intermediate certificate provided by the terminal at the start of the connection.

    You will see a Common Name mismatch error during verification. This is normal, and happens because the certificate's Common Name does not resolve in the DNS.

  2. Validate the Common Name. The Common Name is in one of the following formats:

    • Test terminal Common Name formats:

      • legacy-terminal-certificate.test.terminal.adyen.com
      • [POIID].test.terminal.adyen.com

        [POIID] = [Terminal model]-[Serial number]
        For example P400Plus-123456789.test.terminal.adyen.com

    • Live terminal Common Name formats:
      • legacy-terminal-certificate.live.terminal.adyen.com
      • [POIID].live.terminal.adyen.com

        [POIID] = [Terminal model]-[Serial number]
        For example P400Plus-123456789.live.terminal.adyen.com

    Depending on the number of terminals you are integrating, you may want to use regular expressions in your code to validate the Common Name.

If the certificate on the connected terminal passes verification, your cash register is connected to an Adyen terminal.

Encrypt communications

To prevent others from being able to read requests and responses sent between your cash register and the payment terminal, those messages must be encrypted and signed. The key material for encryption, decryption, and signing is derived from a key that you create and then share with Adyen.

The process involves:

  1. Preparation: Set up a shared key.
  2. Encrypt Terminal API messages before sending.
  3. Decrypt and validate Terminal API messages that you receive.

In the next sections, we use PHP code fragments to explain encryption and decryption step by step. If you want to copy code to your project, refer to the section Full code samples. There you'll find the complete code in several languages, as well as links to GitHub libraries that include this code.

Set up a shared key

The shared key is used to derive key material for encrypting and decrypting requests and responses, and for calculating the digital signature of requests and responses. You need to set up a shared key in your Customer Area. The Adyen payment terminal then receives the shared key with the terminal configuration.

The terminal software can use only one shared key. That means that if you have multiple devices that communicate with the same terminal, these devices must have the same key.

To set up the key:

  1. Log in to your Customer Area.
  2. Go to Point-of-sale > Terminals.
  3. Click the row of the terminal you want to configure. The settings page for that terminal appears.
  4. Select View decrypted properties.
  5. Select Integrations.
  6. Under Encryption key complete the fields:

    • Key identifier: Your identification of the key.
    • Key passphrase: The shared key itself.
    • Key version: The version number of the key. For example, 1.

    If this section is not visible or if you can't edit the fields, ask our POS Support Team to set the required permission for your user.

  7. Select Save.
  8. Securely store the key identifier, passphrase, and version in your system.

Encrypt Terminal API messages

To encrypt Terminal API messages, proceed as follows:

  1. Derive key material by running an HMAC-based key derivation function on the shared key.
  2. Encrypt the message using the derived key material.
  3. Calculate the HMAC signature for the message, using the derived key material.
  4. Create a security trailer that contains the HMAC signature and identifies the shared key that was used for the encryption and the HMAC signature.
  5. Compose a new message containing the encrypted message as a Base64-encoded blob and the security trailer.

The result is a new message ready for sending.

Derive key material

To encrypt and decrypt messages, you need key material consisting of an HMAC key algorithm, a cipher key, and an initialization vector to initialize the encryption and decryption algorithms.

This key material is derived from the shared key passphrase and only changes when the shared key changes. This means you don't need to re-derive key material for each message. However, the derived key material is secret, so if you don't derive key material with every message you need to store the key material securely in your system. Also, you need to add a function to your code to look up the key material.

To derive key material:

  1. Apply the key derivation function PBKDF2-HMAC-SHA1 to the passphrase using the following parameters:

    Parameter Value
    Salt AdyenNexoV1Salt
    Salt length 15
    Rounds 4000
    Key length 80
    function NexoDeriveKeymaterial($passphrase) {
        $outlen = 80;
        $salt =  "AdyenNexoV1Salt";
        $rounds = 4000;
        $bytes = openssl_pbkdf2($passphrase, $salt, $outlen, $rounds, "sha1");
    
        $hmac_key = substr($bytes, 0, 32);
        $cipher_key = substr($bytes, 32, 32);
        $iv = substr($bytes, 64, 16);
    
        return array('hmac_key' => $hmac_key, 'cipher_key' => $cipher_key, 'iv' => $iv);
    }

    This returns a three-element array containing the 32-byte hmac_key, the 32-byte cipher_key and the 16-byte initialization vector iv.

Encrypt the message

  1. Generate a nonce of the same length as the iv (16 bytes).

    PHP
    function NexoDrawNonce() {
        return openssl_random_pseudo_bytes(16);
    }
  2. Add an XOR helper function. You will use this function later with the nonce and the iv from the derived key material as input, to calculate the initialization vector for the encryption and decryption algorithms.

    PHP
    function XorBytes($a, $b) {
        $r = $a;
        for ($i = 0; $i < 16; $i++) {
            $r[$i] = $r[$i] ^ $b[$i];
        }
        return $r;
    }
  3. Encrypt the message with AES265 in CBC mode using the full original message, the derived key material, and the nonce as input.

    PHP
    function NexoEncrypt($message, $keymaterial, $nonce) {
        $realiv = XorBytes($keymaterial['iv'], $nonce);
        return openssl_encrypt($message, "AES-256-CBC", $keymaterial['cipher_key'], OPENSSL_RAW_DATA, $realiv);
    }

Calculate the signature

  1. Calculate the HMAC signature using the full original message and the hmac_key from the derived key material as input.

    PHP
    function NexoHMAC($message, $keymaterial) {
        return hash_hmac("sha256", $message, $keymaterial['hmac_key'], true);
    }

Create the security trailer

  1. Create a security trailer containing the version number and identifier of the shared secret, the nonce, and the HMAC signature.

    PHP
    function NexoTrailer($keyid, $keyversion, $nonce, $hmac) {
        return array('KeyVersion' => $keyversion,
            'KeyIdentifier' => $keyid,
            'Hmac' => base64_encode($hmac),
            'Nonce' => base64_encode($nonce),
            'AdyenCryptoVersion' => 1);
    }

Compose the new message

  1. Create a new message that consists of:

    • The same body key as the original message: SaleToPOIRequest or SaleToPOIResonse.
    • The same MessageHeader as the original message.
    • A Base64-encoded blob with the encrypted original message.
    • The security trailer.
    PHP
    function NexoSender($message, $keyid, $keyversion, $keymaterial) {
        $jsonin = json_decode($message, true);
        $isrequest = isset($jsonin['SaleToPOIRequest']);
        $bodykey = $isrequest ? 'SaleToPOIRequest' : 'SaleToPOIResponse';
        $body = $jsonin[$bodykey];
    
        $header = $body['MessageHeader'];
    
        // Encrypt the original message and compute its hmac signature
        $nonce = NexoDrawNonce();
        $nexoblob = NexoEncrypt($message, $keymaterial, $nonce);
        $hmac = NexoHMAC($message, $keymaterial);
    
        $trailer = NexoTrailer($keyid, $keyversion, $nonce, $hmac);
    
        // The result has three parts: header, blob, and trailer
        $result = array('MessageHeader' => $header, 'NexoBlob' => base64_encode($nexoblob), 'SecurityTrailer' => $trailer);
    
        return json_encode(array($bodykey => $result), JSON_PRETTY_PRINT);
    }

    The result is the new message, ready for sending. Here is an example:

    {
        "SaleToPOIRequest":{
            "MessageHeader":{
                "MessageClass":"Service",
                "ProtocolVersion":"3.0",
                "ServiceID":"6158",
                "MessageCategory":"Abort",
                "SaleID":"POSSystemID12345",
                "MessageType":"Request",
                "POIID":"M400-260193322"
            },
            "NexoBlob":"ae8b41wcH9ZH18CRTHSPXi4FdN5Hd2vOQ9ZTKS+GsHvXFqyrAPtVZtmlyI5fWzxpzLMYOyZIAbaSFuasmGi2WcvFO5DBIWvstaQyIfDgcs9oVCuSWvgLXqnCocV8juZNjYGWllY1t0HKuym0I1lCeQRPehzyNbQn5aUp7fr6AuUTgLC+bAZWh/DqnxCCW5wcyNq9QFC8H+1Gm9R4weJH8zEBMTxldh1BDwp/5Xabz5nkfvDYranT463PTw9czge5VcgE7sGaBLaMWzYU9HI9QVlShceasOZo18rohNRdeaJVuJ1JJme2ZY1ZWav44rXi77NN3QuC5mbj0bUCKFOhTCOVcxKdNlIlmF0tAmCcmNiPwmSLL6kmygZNcgQ7zKjWVJsFhQ+2I4hOWCE4ZbJ6jAxyGbLnCpSjzhfFpLBQvGRuFiaCNMNbAUh2iL9Ep6jMlf5/SqpYXhji+8hQF8jXMF9i6oYJ1G/WUQRSajVklpHk7KoTpH2JjtuG7jZmPxzVGj1/vKPSaT90WiVPOay1vMLKb6V3Tc+DpjG0Y2sNj+bc6PvqnXmUPPlyiA+I65XkawdXR3qcsm2AFNLRTLZR4Q0og5FXXpZxOBFtfmDNFQ+Ygtb/JqsB960HaWhQkAyBxZKJ0nfWBeriiF4t1c6ppgejqmIqxAauOmAJjng+5hcA3x2kSFl9MT6kGx21Kt04ijAFX7OTyfBggJFEhnphHQ==",
            "SecurityTrailer":{
                "KeyVersion":0,
                "KeyIdentifier":"mykey",
                "Hmac":"h6ehPJOASK4NXGESERmXo5mP9YFxpox7VoAFGIb9s8Y=",
                "Nonce":"BoBZRF2QmDlNnmeo1QYeZQ==",
                "AdyenCryptoVersion":1
            }
        }
    }

Decrypt Terminal API messages

Decrypting messages requires the following steps:

  1. Decompose the received message.
  2. Identify the shared key and use that to derive key material.
  3. Decrypt the original message using the derived key material
  4. Validate the message by checking that the HMAC signature and MessageHeader of the received message are the same as those of the decrypted original message.

The result is the decrypted original message ready for processing.

  1. Create some functions you'll need later on in the NexoReceiver function:

    • A function to look up the passphrase.
    PHP
    function NexoLookupKeybyIdAndVersion($keyid, $keyversion) {
        // This function should do a lookup based on key id and version.
        // But for demonstration purposes, we just return a given test passphrase.
        return NexoDeriveKeyMaterial("mysupersecretpassphrase");
    }
    • A function to decrypt the message with AES265 in CBC mode. This function reuses the XorBytes function from the encryption steps to set the initialization vector for the decryption algorithm.
    PHP
    function NexoDecrypt($message, $keymaterial, $nonce) {
        $realiv = XorBytes($keymaterial['iv'], $nonce);
        return openssl_decrypt($message, "AES-256-CBC", $keymaterial['cipher_key'], OPENSSL_RAW_DATA, $realiv);
    }

Decompose the message

  1. Parse the received message and decompose it into three parts::

    • The MessageHeader.
    • The blob of the encrypted original message.
    • The security trailer.
    PHP
    function NexoReceiver($message) {
        $jsonin = json_decode($message, true);
        $isrequest = isset($jsonin['SaleToPOIRequest']);
        $bodykey = $isrequest ? 'SaleToPOIRequest' : 'SaleToPOIResponse';
        $body = $jsonin[$bodykey];
        $header = $body['MessageHeader'];
        $blob = $body['NexoBlob'];
        $trailer = $body['SecurityTrailer'];
        // NexoReceiver function continues in the next steps

Derive key material

  1. Look up the passphrase of the shared key based on the version number and identifier in the security trailer, and Base64-decode the HMAC signature and the nonce from the security trailer.

    PHP
        if ($trailer['AdyenCryptoVersion'] != 1) {
            return null;
        }
        $keymaterial = NexoLookupKeybyIdAndVersion($trailer['KeyIdentifier'], $trailer['KeyVersion']);
        $nonce = base64_decode($trailer['Nonce']);
        $hmac = base64_decode($trailer['Hmac']);
  2. Reuse the NexoDeriveKeymaterial function to derive the key material based on the passphrase.

Decrypt the original message

  1. Base64-decode the blob and decrypt the original message using the cipher_key and the nonce as input, and reusing the XorBytes function from the encryption steps to set the initialization vector for the decryption algorithm.

    PHP
        $nexoblob = base64_decode($body['NexoBlob']);
        $decrypted = NexoDecrypt($nexoblob, $keymaterial, $nonce);

Validate the message

  1. Validate the HMAC signature: Base64-decode the HMAC from the received message, reuse the NexoHMAC function to calculate the HMAC of the decrypted message, and compare the two.

    PHP
        $computed_hmac = NexoHMAC($decrypted, $keymaterial);
        if ($computed_hmac != $hmac) {
            return null;
        }
  2. Verify that the plaintext MessageHeader matches the MessageHeader in the decrypted message, and then return the decrypted message.

    PHP
        $decrypted_json = json_decode($decrypted, true);
        if ($decrypted_json[$bodykey]['MessageHeader'] !== $header) {
            return null;
        }
        return $decrypted;
    }
    // end of the NexoReceiver function

    If the validation succeeds, the result is the decrypted message, ready for processing.

Full code samples

The next examples show the full code for encrypting and decrypting messages. This code is also available in the C# library and Java library on our GitHub.

import java.io.*;
import java.security.*;
import java.security.spec.*;
import java.text.ParseException;
import java.util.Base64;
import java.util.Random;
import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import javax.json.*;

public class NexoCrypto {

    public static final int NEXO_HMAC_KEY_LENGTH = 32;
    public static final int NEXO_CIPHER_KEY_LENGTH = 32;
    public static final int NEXO_IV_LENGTH = 16;

    /**
     * Where to look for pre-derived key files
     */
    private String keyDirectory;

    /**
     * The key material to use when sending
     */
    private String keyIdentifier;
    private long keyVersion;
    private NexoDerivedKeys derivedKeys;

    /**
     * A container for Nexo derived keys
     *
     * Nexo derived keys is a 80 byte struct containing key data. These 80
     * bytes are derived from a passsphrase.
     */
    public class NexoDerivedKeys {
        public byte hmac_key[];
        public byte cipher_key[];
        public byte iv[];

        public NexoDerivedKeys() {
            hmac_key = new byte[NEXO_HMAC_KEY_LENGTH];
            cipher_key = new byte[NEXO_CIPHER_KEY_LENGTH];
            iv = new byte[NEXO_IV_LENGTH];
        }

        /**
         * Read a key material file of 80 bytes, splitting it in the hmac_key, cipher_key and iv
         */
        public void readKeyData(String keyId, long keyV) throws IOException {
            String filename = keyDirectory + '/' + keyId + "." + Long.toString(keyV) + ".key";

            FileInputStream stream = null;
            try {
                stream = new FileInputStream(filename);
                stream.read(this.hmac_key);
                stream.read(this.cipher_key);
                stream.read(this.iv);
            } finally {
                if (stream != null) {
                    try {
                        stream.close();
                    }
                    catch (Exception ignored) {
                    }
                }
            }
        }

    };

    /**
     * Use this constructor if you want to do both encryption and decryption
     */
    public NexoCrypto(String dir, String keyID, long keyV) throws IOException {
        keyDirectory = dir;
        keyIdentifier = keyID;
        keyVersion = keyV;
        derivedKeys = new NexoDerivedKeys();
        derivedKeys.readKeyData(keyIdentifier, keyVersion);
    }

    /**
     * Use this constructor if you want to decrypt-only NexoCrypto object
     */
    public NexoCrypto(String dir) {
        keyDirectory = dir;
    }

    /**
     * Given a passphrase, compute 80 byte key of key material according to crypto.md
     */
    public static byte[] deriveKeyMaterial(char[] passphrase) throws NoSuchAlgorithmException, InvalidKeySpecException {
        byte[] salt = "AdyenNexoV1Salt".getBytes();
        int iterations = 4000;
        PBEKeySpec spec = new PBEKeySpec(passphrase, salt, iterations, (NEXO_HMAC_KEY_LENGTH +
            NEXO_CIPHER_KEY_LENGTH + NEXO_IV_LENGTH) * 8);
        SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
        byte[] keymaterial = skf.generateSecret(spec).getEncoded();
        return keymaterial;
    }

    /**
     * Encrypt or decrypt data given an iv modifier and using the specified key
     * The actual iv is computed by taking the iv from the key material and xoring it with ivmod
     */
    private byte[] crypt(byte[] bytes, NexoDerivedKeys dk, byte[] ivmod, int mode)
        throws NoSuchAlgorithmException, NoSuchPaddingException,
        IllegalBlockSizeException, BadPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {

        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        SecretKeySpec s = new SecretKeySpec(dk.cipher_key, "AES");

        // xor dk.iv and the iv modifier
        byte[] actualIV = new byte[NEXO_IV_LENGTH];
        for (int i = 0; i < NEXO_IV_LENGTH; i++) {
            actualIV[i] = (byte) (dk.iv[i] ^ ivmod[i]);
        }

        IvParameterSpec i = new IvParameterSpec(actualIV);
        cipher.init(mode, s, i);
        return cipher.doFinal(bytes);
    }

    /**
     * Compute an hmac using the hmac_key
     */
    private byte[] hmac(byte[] bytes, NexoDerivedKeys dk) throws NoSuchAlgorithmException, InvalidKeyException {
        Mac mac = Mac.getInstance("HmacSHA256");
        SecretKeySpec s = new SecretKeySpec(dk.hmac_key, "HmacSHA256");

        mac.init(s);
        return mac.doFinal(bytes);
    }

    /**
     * Encrypt and compose a secured Nexo message
     *
     * This functions takes the original message, encrypts it and converts the encrypted form to Base64 and
     * names it NexoBlob.
     * After that, a new message is created with a copy of the header, the NexoBlob, and an added SecurityTrailer.
     *
     * @param in is the byte representation of the unprotected Nexo message
     * @returns a byte representation of the secured Nexo message
     */
    public byte[] encrypt_and_hmac(byte in[]) throws InvalidKeyException, NoSuchAlgorithmException,
        NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException,
        InvalidAlgorithmParameterException {
        Base64.Encoder encb64 = Base64.getEncoder();

        // parse the json and determine if it is a request or responce
        JsonReader jsonreader = Json.createReader(new ByteArrayInputStream(in));
        JsonObject body = jsonreader.readObject();
        boolean request = true;
        JsonObject saletopoirequest = body.getJsonObject("SaleToPOIRequest");
        if (saletopoirequest == null) {
            request = false;
            saletopoirequest = body.getJsonObject("SaleToPOIResponse");
        }
        // pick up the MessageHeader
        JsonObject messageheader = saletopoirequest.getJsonObject("MessageHeader");

        // Generate a random iv nonce
        byte[] ivmod = new byte[NEXO_IV_LENGTH];
        new Random().nextBytes(ivmod);

        // encrypt taking the original bytes as input
        byte[] encbytes = crypt(in, this.derivedKeys, ivmod, Cipher.ENCRYPT_MODE);

        // compute mac over cleartext bytes
        byte[] hmac = hmac(in, this.derivedKeys);

        // Construct the inner Json object containing a MessageHeader, a NexoBlob and a SecurityTrailer
        JsonObject msg = Json.createObjectBuilder()
            .add("MessageHeader", messageheader)
            .add("NexoBlob", new String(encb64.encode(encbytes)))
            .add("SecurityTrailer", Json.createObjectBuilder()
                .add("Hmac", new String(encb64.encode(hmac)))
                .add("KeyIdentifier", keyIdentifier)
                .add("KeyVersion", keyVersion)
                .add("AdyenCryptoVersion", 1)
                .add("Nonce", new String(encb64.encode(ivmod)))
            ).build();
        // Wrap the inner message in a SaleToPOIRequest or SaleToPOIResponse object
        JsonObject total = Json.createObjectBuilder()
            .add(request ?  "SaleToPOIRequest" : "SaleToPOIResponse" , msg)
            .build();

        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        JsonWriter writer = Json.createWriter(stream);
        writer.writeObject(total);
        writer.close();
        return stream.toByteArray();
    }

    /**
     * A helper class to return a decrypted mesasage and the outer header from the
     * secured Nexo message
     */
    public class BytesAndOuterHeader {
        public byte[] packet;
        public JsonObject outer_header;

        public BytesAndOuterHeader(byte[] packet, JsonObject outer_header) {
            this.packet = packet;
            this.outer_header = outer_header;
        }
    }

    /**
     * Validate and decrypt a secured Nexo message
     * @returns a BytesAndOuterHeader object or null on failure
     */
    public BytesAndOuterHeader decrypt_and_validate_hmac(byte in[]) throws InvalidKeyException,
        NoSuchAlgorithmException, IOException, NoSuchPaddingException, IllegalBlockSizeException,
        BadPaddingException, InvalidAlgorithmParameterException {
        Base64.Decoder b64dec = Base64.getDecoder();

        // Parse bytes and retrieve MessageHeader
        InputStream stream = new ByteArrayInputStream(in);
        JsonReader jsonreader = Json.createReader(stream);
        JsonObject total = jsonreader.readObject();
        if (total == null) {
            throw new IOException("Faulty JSON");
        }
        JsonObject saletopoirequest = total.getJsonObject("SaleToPOIRequest");
        if (saletopoirequest == null) {
            saletopoirequest = total.getJsonObject("SaleToPOIResponse");
        }
        if (saletopoirequest == null) {
            throw new IOException("No SaleToPOIRequest or SaleToPOIResponse");
        }
        JsonObject messageheader = saletopoirequest.getJsonObject("MessageHeader");
        if (messageheader == null) {
            throw new IOException("MessageHeader not found");
        }
        // Get the encrypted actual message and base64 decode it
        JsonString payload = saletopoirequest.getJsonString("NexoBlob");
        if (payload == null) {
            throw new IOException("NexoBlob not found");
        }
        byte[] ciphertext = b64dec.decode(payload.getString());

        // Get the SecurityTrailer and its values
        JsonObject jsonTrailer = saletopoirequest.getJsonObject("SecurityTrailer");
        if (jsonTrailer == null) {
            throw new IOException("SecurityTrailer not found");
        }
        JsonNumber version = jsonTrailer.getJsonNumber("AdyenCryptoVersion");
        if (version == null || version.intValue() != 1) {
            throw new IOException("AdyenCryptoVersion version not found or not supported");
        }
        JsonString nonce = jsonTrailer.getJsonString("Nonce");
        if (nonce == null) {
            throw new IOException("Nonce not found");
        }

        JsonString keyId = jsonTrailer.getJsonString("KeyIdentifier");
        if (keyId == null) {
            throw new IOException("KeyIdentifier not found");
        }
        JsonNumber kversion = jsonTrailer.getJsonNumber("KeyVersion");
        if (kversion == null) {
            throw new IOException("KeyVersion not found");
        }
        JsonString b64 = jsonTrailer.getJsonString("Hmac");
        if (b64 == null) {
            throw new IOException("Hmac not found");
        }

        // Read the key from disk
        NexoDerivedKeys dk = new NexoDerivedKeys();
        dk.readKeyData(keyId.getString(), kversion.longValue());

        // Decrypt the actual message with the base64 decoded ivmod as found in the securitytrailer
        byte[] ivmod = b64dec.decode(nonce.getString());
        byte[] ret = crypt(ciphertext, dk, ivmod, Cipher.DECRYPT_MODE);

        // Base64 decode the received HMAC and compare it to a computed hmac
        // Use a timing safe compare, this is to mitigate a (theoretical) timing based attack
        byte[] receivedmac = b64dec.decode(b64.getString());
        byte[] hmac = hmac(ret, dk);
        if (receivedmac.length != hmac.length) {
            throw new IOException("Validation failed");
        }
        boolean equal = true;
        for (int i = 0; i < hmac.length; i++) {
            if (receivedmac[i] != hmac[i]) {
                equal = false;
            }
        }
        if (!equal) {
            throw new IOException("Validation failed");
        }

        // Return decrypted message and outer header
        return new BytesAndOuterHeader(ret, messageheader);
    }

    /**
     * Compare an inner and outer MessageHeader
     * @param the inner  MessageHeader
     * @param outer the outer MessageHeader
     * @return
     */
    public boolean validateInnerAndOuterHeader(JsonObject inner, JsonObject outer) {
        if (inner == null || outer == null) {
            return false;
        }
        String[] fields = {
            "DeviceID",
            "MessageCategory",
            "MessageClass",
            "MessageType",
            "SaleID",
            "ServiceID",
            "POIID",
            "ProtocolVersion",
        };
        for (String field : fields) {

            try {
                JsonString a = inner.getJsonString(field);
                JsonString b = outer.getJsonString(field);
                if (a == null && b == null) {
                    continue;
                }
                if (a == null || !a.equals(b)) {
                    return false;
                }
            }
            catch (ClassCastException ex) {
                return false;
            }
        }
        return true;
    }
}
/**
 * Derive key material given a passphrase.
 * @var $passphrase string
 * @returns a 3-element array containing the derived key material
 */
function NexoDeriveKeymaterial($passphrase) {
    $outlen = 80;
    $salt =  "AdyenNexoV1Salt";
    $rounds = 4000;
    $bytes = openssl_pbkdf2($passphrase, $salt, $outlen, $rounds, "sha1");

    $hmac_key = substr($bytes, 0, 32);
    $cipher_key = substr($bytes, 32, 32);
    $iv = substr($bytes, 64, 16);

    return array('hmac_key' => $hmac_key, 'cipher_key' => $cipher_key, 'iv' => $iv);
}

/**
 * Take a plaintext Nexo message and create a secure message from it.
 * @var $message string the input Nexo message in serialized form
 * @var $keyid the key identifier
 * @var $keyversion int the version of the key. The pair $keyid-keyversion identifies a key
 * @var $keymaterial array an array as returned by NexoDeriveKeymaterial()
 * @returns a secured, serizled Nexo message
 */
function NexoSender($message, $keyid, $keyversion, $keymaterial) {
    $jsonin = json_decode($message, true);
    $isrequest = isset($jsonin['SaleToPOIRequest']);
    $bodykey = $isrequest ? 'SaleToPOIRequest' : 'SaleToPOIResponse';
    $body = $jsonin[$bodykey];

    $header = $body['MessageHeader'];

    // Encrypt the original message and compute its hmac
    $nonce = openssl_random_pseudo_bytes(16);
    $nexoblob = NexoEncrypt($message, $keymaterial, $nonce);
    $hmac = NexoHMAC($message, $keymaterial);

    $trailer = NexoTrailer($keyid, $keyversion, $nonce, $hmac);

    // result has three parts: header, blob and trailer
    $result = array('MessageHeader' => $header, 'NexoBlob' => base64_encode($nexoblob), 'SecurityTrailer' => $trailer);

    return json_encode(array($bodykey => $result), JSON_PRETTY_PRINT);
}

/**
 * Take a secured Nexo message, decrypt and validate it.
 * @var @message string a serialized, secured Nexo message
 * @returns string or a serialzed nexo message or null if validaton failed
 */
function NexoReceiver($message) {
    // Warning: almost all validation is missing!
    // Parse the incoming message and decompose it
    $jsonin = json_decode($message, true);
    $isrequest = isset($jsonin['SaleToPOIRequest']);
    $bodykey = $isrequest ? 'SaleToPOIRequest' : 'SaleToPOIResponse';
    $body = $jsonin[$bodykey];
    $blob = $body['NexoBlob'];
    $header = $body['MessageHeader'];
    $trailer = $body['SecurityTrailer'];

    // Get the information from the SecurityTrailer
    if ($trailer['AdyenCryptoVersion'] != 1) {
        return null;
    }
    $keymaterial = NexoLookupKeybyIdAndVersion($trailer['KeyIdentifier'], $trailer['KeyVersion']);
    $nonce = base64_decode($trailer['Nonce']);
    $hmac = base64_decode($trailer['Hmac']);

    // Decrypt the blob
    $nexoblob = base64_decode($body['NexoBlob']);
    $decrypted = NexoDecrypt($nexoblob, $keymaterial, $nonce);

    // Validate the received hmac against the computed hmac
    $computed_hmac = NexoHMac($decrypted, $keymaterial);
    if ($computed_hmac != $hmac) {
        return null;
    }

    // Make sure the plaintext header and the header in the decrypted message match
    $decrypted_json = json_decode($decrypted, true);
    if ($decrypted_json[$bodykey]['MessageHeader'] !== $header) {
        return null;
    }
    return $decrypted;
}

// Basic encryption/decryption: AES-256-CBC with default padding
function NexoEncrypt($message, $keymaterial, $nonce) {
    $realiv = XorBytes($keymaterial['iv'], $nonce);
    return openssl_encrypt($message, "AES-256-CBC", $keymaterial['cipher_key'], OPENSSL_RAW_DATA, $realiv);
}

function NexoDecrypt($message, $keymaterial, $nonce) {
    $realiv = XorBytes($keymaterial['iv'], $nonce);
    return openssl_decrypt($message, "AES-256-CBC", $keymaterial['cipher_key'], OPENSSL_RAW_DATA, $realiv);
}

// The autentication function, standard SHA256 HMAC
function NexoHMAC($message, $keymaterial) {
    return hash_hmac("sha256", $message, $keymaterial['hmac_key'], true);
}

// Helper funtion: construct a SecurityTrailer
function NexoTrailer($keyid, $keyversion, $nonce, $hmac) {
    return array('KeyVersion' => $keyversion,
        'KeyIdentifier' => $keyid,
        'Hmac' => base64_encode($hmac),
        'Nonce' => base64_encode($nonce),
        'AdyenCryptoVersion' => 1);
}

// Helper funtion: xor two arrays of bytes
function XorBytes($a, $b) {
    $r = $a;
    for ($i = 0; $i < 16; $i++) {
        $r[$i] = $r[$i] ^ $b[$i];
    }
    return $r;
}

// Lookup key material based on keyIdentifier and KeyVersion
function NexoLookupKeybyIdAndVersion($keyid, $keyversion) {
    // Actually, this function should do a lookup based on key id and version.
    // But for demonstration purposes we just return the derived keymaterial for
    // the given test passphrase.
    return NexoDeriveKeyMaterial("mysupersecretpassphrase");
}
const crypto = require('crypto');

const NEXO_HMAC_KEY_LENGTH = 32;
const NEXO_CIPHER_KEY_LENGTH = 32;
const NEXO_IV_LENGTH = 16;

class NexoCrypto {

    constructor() {

    }

    configure(passphrase, keyIdentifier, keyVersion) {
        if (passphrase) {
            this.derivedKeys = this.deriveKeyMaterial(passphrase);
        }

        this.keyIdentifier = keyIdentifier;
        this.keyVersion = keyVersion || 0;
    }

    /**
     * Derive key material given a passphrase.
     * @var $passphrase string
     * @returns a 3-element array containing the derived key material
     */
    deriveKeyMaterial(passphrase) {

        var pass = Buffer.from(passphrase, 'binary');
        const salt =  Buffer.from("AdyenNexoV1Salt", 'binary');
        const iterations = 4000;
        const keylen = NEXO_HMAC_KEY_LENGTH + NEXO_CIPHER_KEY_LENGTH + NEXO_IV_LENGTH;

        const key = crypto.pbkdf2Sync(pass, salt, iterations, keylen, 'sha1');

        var ret = {
            key: key,
            hmac_key: key.slice(0, 32),
            cipher_key: key.slice(32, 64),
            iv: key.slice(64, 80)
        };
        return ret;
    }

    /**
     * Encrypt or decrypt data given a iv modifier and using the specified key
     * The actual iv is computed by taking the iv from the key material and xoring it with ivmod
     */
    crypt(bytes, dk, ivmod, encrypt) {

        // xor dk.iv and the iv modifier
        var actualIV = Buffer.alloc(NEXO_IV_LENGTH);
        for (var i = 0; i < NEXO_IV_LENGTH; i++) {
             actualIV[i] = dk.iv[i] ^ ivmod[i];
        }

        var cipher;
        if (encrypt) {
            cipher = crypto.createCipheriv('aes-256-cbc', dk.cipher_key, actualIV);
        } else {
            cipher = crypto.createDecipheriv('aes-256-cbc', dk.cipher_key, actualIV);
        }

        var data = cipher.update(bytes);
    data = Buffer.concat([data, cipher.final()]);

        return data;
    }

    /**
     * Compute a hmac using the hmac_key
     */
    hmac(bytes, dk) {
        var mac = crypto.createHmac('sha256', dk.hmac_key)
        var hmac = mac.update(bytes).digest(); //hex ?
        return hmac;
    }

    /**
     * Encrypt and compose a secured Nexo message
     *
     * This functions takes the original message, encrypts it and converts the encrypted form to Base64 and
     * names it NexoBlob.
     * After that, a new message is created with a copy of the header, the NexoBlob and an added SecurityTrailer.
     *
     * @param in is the byte representation of the unprotected Nexo message
     * @returns a byte representation of the secured Nexo message
     */
     encrypt_and_hmac(bytes) {

         // parse the json
         var body = JSON.parse(bytes);

         // and determined if it is a request or responce
         var request = true;
         var saletopoirequest = body["SaleToPOIRequest"];
         if (!saletopoirequest) {
                request = false;
                saletopoirequest = body["SaleToPOIResponse"];
         }

         // pick up the MessageHeader
         var messageHeader = saletopoirequest["MessageHeader"];

         // Generate a random iv nonce
         var ivmod = (this.derivedKeys.nonce) ? this.derivedKeys.nonce : crypto.randomBytes(NEXO_IV_LENGTH);

         // encrypt taking the original bytes as input
         var encbytes = this.crypt(bytes, this.derivedKeys, ivmod, true);

         // compute mac over cleartext bytes
         var hmac = this.hmac(bytes, this.derivedKeys);

         // Construct the inner Json object containing a MessageHeader, a NexoBlob and a SecurityTrailer

         var msg = {
                MessageHeader: messageHeader,
                NexoBlob: encbytes.toString('base64'),
                SecurityTrailer: {
                    Hmac: hmac.toString('base64'),
                    KeyIdentifier: this.keyIdentifier,
                    KeyVersion: parseInt(this.keyVersion),
                    AdyenCryptoVersion: 1,
                    Nonce: ivmod.toString('base64')
                }
            };

         // Wrap the inner message in a SaleToPOIRequest or SaleToPOIResponse object
         var reqWrap = (request) ? "SaleToPOIRequest" : "SaleToPOIResponse"
         var total = {};
         total[reqWrap] = msg;

         return JSON.stringify(total);
     }

     /*
    * Validate and decrypt a secured Nexo message
    *
    */
     decrypt_and_validate_hmac(bytes) {
         // parse the json
         var body = JSON.parse(bytes);

         // and determined if it is a request or responce
         var request = true;
         var saletopoirequest = body["SaleToPOIRequest"];
         if (!saletopoirequest) {
                request = false;
                saletopoirequest = body["SaleToPOIResponse"];
         }

         // pick up the MessageHeader
         var messageHeader = saletopoirequest["MessageHeader"];
         var payload = saletopoirequest["NexoBlob"];
         var ciphertext = Buffer.from(payload, 'base64');

         // Get the SecurityTrailer and its values
         var jsonTrailer = saletopoirequest["SecurityTrailer"];
         var version = jsonTrailer["AdyenCryptoVersion"];

         var nonceB64 = jsonTrailer["Nonce"];
         var ivmod = Buffer.from(nonceB64, 'base64');

         var keyId = jsonTrailer["KeyIdentifier"];
         var kversion = jsonTrailer["KeyVersion"];
         var hmacB64 = jsonTrailer["Hmac"];

         var ret = this.crypt(ciphertext, this.derivedKeys, ivmod, false);
         var json = JSON.parse(ret);

         // Base64 decode the received HMAC and compare it to a computed hmac
         // Use a timing safe compare, this is to mitigate a (theoretical) timing based attack
         var receivedmac = Buffer.from(hmacB64, 'base64');
         var hmac = this.hmac(ret, this.derivedKeys);

         //console.log(receivedmac);
         //console.log(hmac);

         if (receivedmac.length != hmac.length) {
                // console.log("HMAC Validation failed - Length mismatch");
                return;
         }
         var equal = true;
         for (var i = 0; i < hmac.length; i++) {
                if (receivedmac[i] != hmac[i]) {
                     equal = false;
                }
         }
         if (!equal) {
                // console.log("HMAC Validation failed - Not Equal");
                return;
         }

         return JSON.stringify(json);
     }
}

See also