Search

Are you looking for test card numbers?

Would you like to contact support?

Secure local communications

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

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

To secure 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. Prepare a trust store on the cash register.
  2. Authenticate the connection between the cash register and 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:

    These certificates can be converted to formats such as .crt, .cer, or .p12, if needed.

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

    Follow your vendor's instructions for adding root certificates to either the system or user trust store.
    For example, Microsoft's documentation for adding certificates using the MMC snap-in, or importing certificates with the PowerShell.

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 check includes automatic 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 communications between your cash register and terminal, you need to encrypt messages being sent between these devices. Encrypting these communications involves:

  1. Deriving an encryption key.
  2. Encrypting and decrypting messages.

Step 1: Derive encryption key

Derive an encryption key to secure communication between devices using the Terminal API.

To derive an encryption key, the two parties must:

  1. Share a variable length secret to derive the key material. 
  2. Derive the key using PBKDF2_HMAC_SHA1
  3. Include the following parameters:

    Parameter Value
    salt AdyenNexoV1Salt
    salt length 15;
    rounds 4000;

    If the program saves the resulting derived key, this computation is performed once for each shared secret.

    The derived key material consists of 80 bytes: a 32 byte cipher key, a 32 byte HMAC key and a 16 byte initialization vector (IV). The IV is XORed with a random nonce that is generated per message on encryption. 

    A derived key is identified by both its KeyIdentifier and its version.

The following example C code demonstrates how you would use OpenSSL to derive the key material:

/* The struct to hold the derived keys is defined like so: */
 
struct nexo_derived_keys {
    uint8_t hmac_key[NEXO_HMAC_KEY_LENGTH];
    uint8_t cipher_key[NEXO_CIPHER_KEY_LENGTH];
    uint8_t iv[NEXO_IV_LENGTH];
};

/* Function to derive the keys: */
int nexo_derive_key_material(const char * passphrase, size_t len, struct nexo_derived_keys *dk) {
    const unsigned char salt[] = "AdyenNexoV1Salt";
    const int saltlen = sizeof(salt) - 1;
    const int rounds = 4000;
    return PKCS5_PBKDF2_HMAC_SHA1(passphrase, len, salt, saltlen, rounds,
             sizeof(struct nexo_derived_keys), (unsigned char *)dk);

}

Step 2: Encrypt and decrypt messages

Configure encryption in the Terminal API to protect messages communicated between the terminal and the cash register.

Encrypt messages

To encrypt a message:

  1. Encrypt the body of the message using a Nonce XOR dk.iv initialization vector (IV), where dk.iv is the IV in the derived key material.
    The nonce (number used once), is a random sequence of bytes of the same length as dk.iv generated per message.
  2. Place the nonce in the SecurityTrailer as a Base64 encoded string.

Decrypt and validate messages

To decrypt and validate a message:

  1. Decompose the message into its various parts. 
  2. Check the AdyenCryptoVersion in the SecurityTrailer.
  3. Retrieve the key material based on the KeyIdentifier and KeyVersion.
  4. Decrypt the blob using the IV and key in the key material and the Nonce in the SecurityTrailer.
  5. Validate the HMAC by computing it on the on the decrypted blob.

    Do not validate on the serialized form.

  6. Compare the result to the blob in the SecurityTrailer. The message contains a clear text copy of the MessageHeader for routing purposes. 
  7. Pairwise compare the clear text MessageHeader to the MessageHeader in the decrypted body and establish that they are not the same as the MessageHeader in the decrypted body. 

Configure a key in the Customer Area

The key the terminal uses, retrieved with the terminal configuration, is set in the Customer Area. Currently the terminal software is capable of using one key only. If multiple devices are communicating with the same terminal, they must share the same key.

To configure the key:

  1. Open the Customer Area and go to Point-of-sale > Terminals
  2. Click the row of the terminal you want to configure. The configuration page for that terminal will open.
  3. Click View decrypted properties.
    The page refreshes and returns you to terminal API menu.
  4. Click the Terminal API tab.
  5. Click Edit.
  6. Enter the encryption key identifier, and key passphrase.
  7. Click Save
  8. Add your key in the Security Keys field.

The following is an example of the Security Keys property:

{  
   "defaultKey":{  
      "passphrase":"mysupersecretpassphrase",
      "keyIdentifier":"mykey",
      "version":0
   }
}

Encryption example

The example code below implements message encryption, HMAC computation, and construction plus the reverse operation: splitting, HMAC validation and decryption.

By default, Java has AES 128 restriction. You may need to download a new policy file to enable AES 256, otherwise the Illegal key exception will be thrown.

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 keys material to use when we are 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 be able to 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 a 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 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
    */
   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 a 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 determined 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;
   }

}

See also