Point-of-sale icon

Comunicações locais seguras

Valide e criptografe as comunicações locais entre sua caixa registradora e o terminal.

Se sua integração usar comunicações locais, você precisará executar algumas etapas adicionais para ajudar a proteger sua integração contra ataques, interceptação e adulteração.

Para proteger as comunicações entre sua caixa registradora e o terminal, você precisa:

Validar certificado do terminal

Todo terminal da Adyen vem pré-instalado com um certificado assinado pela Adyen. Para confirmar que sua caixa registradora está se comunicando diretamente com um terminal da Adyen, é necessário:

  1. Instalar o certificado raiz público da Adyen trust store.
  2. Autenticar a conexão entre a caixa registradora e o terminal, verificando o certificado no terminal .

Etapa 1: Instalar o certificado

Para instalar o certificado raiz público da Adyen em seu trust store:

  1. Faça o download dos certificados raiz públicos da Adyen em sua caixa registradora:

    Esses certificados podem ser convertidos para formatos como .crt, .cer, ou .p12, se necessário.

  2. Verifique se a assinatura SHA-256 nos certificados corresponde ao seguinte:

    • Certificado de terminal TEST: 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
    • Certificado de terminal LIVE: 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

    Em um ambiente Windows a impressão digital SHA-256 pode não estar prontamente disponível por código ou MMC. Nesse caso, verifique a assinatura SHA-1 nos certificados:

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

  3. Instale os certificados no trust store da sua caixa registradora.

    Siga as instruções do fornecedor para adicionar certificados raiz ao trust store do sistema ou do usuário.
    Por exemplo, a documentação da Microsoft para adicionar certificados usando o snap-in MMC, ou importar certificados com o PowerShell.
    Para iOS, consulte: Manipulação de certificado iOS.

Depois de instalar os certificados em seu trust store, verifique o certificado no terminal da sua caixa registradora.

Etapa 2: Verificar o certificado

Quando sua caixa registradora se conecta ao terminal, ela deve:

  1. Verificar se o certificado no terminal está assinado pelo certificado raiz confiável que você instalou. Essa verificação inclui a verificação automática do certificado intermediário fornecido pelo terminal no início da conexão.

    Você verá um erro de incompatibilidade de nome comum durante a verificação. Isso é normal e acontece porque o Nome Comum do certificado não é resolvido no DNS.

  2. Valide o nome comum. O nome comum está em um dos seguintes formatos:

    • Formatos de nome comum do terminal TEST:

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

        [POIID] = [Modelo do terminal]-[Número de série]
        Por exemplo, P400Plus-123456789.test.terminal.adyen.com

    • Formatos de nome comum do terminal LIVE:

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

        [POIID] = [Modelo do terminal]-[Número de série]
        For example P400Plus-123456789.live.terminal.adyen.com

    Dependendo do número de terminais que você está integrando, convém usar expressões regulares no seu código para validar o Nome Comum.

Se o certificado no terminal conectado passar na verificação, sua caixa registradora será conectada a um terminal Adyen.

Criptografar comunicações

Para impedir que outras pessoas possam ler as comunicações entre sua caixa registradora e o terminal, é necessário criptografar as mensagens enviadas entre esses dispositivos. Criptografar essas comunicações envolve:

  1. Derivar uma chave de criptografia.
  2. Criptografar e descriptografar mensagens.

Etapa 1: Derivar chave de criptografia

Derive uma chave de criptografia para proteger a comunicação entre dispositivos usando a API do Terminal.

Para derivar uma chave de criptografia, as duas partes devem:

  1. Compartilhar um segredo de comprimento variável para obter o material principal.
  2. Derivar a chave usando PBKDF2_HMAC_SHA1
  3. Incluir os seguintes parâmetros:

    Parâmetro Valor
    salt AdyenNexoV1Salt
    salt length 15;
    rounds 4000;

    Se o programa salvar a chave derivada resultante, esse cálculo será realizado uma vez para cada segredo compartilhado.

    O material da chave derivada consiste em 80 bytes: uma chave de cifra de 32 bytes, uma chave HMAC de 32 bytes e um vetor de inicialização (IV) de 16 bytes. O IV é XORed com um nonce aleatório que é gerado por mensagem na criptografia.

    Uma chave derivada é identificada por sua KeyIdentifier e sua versão.

O exemplo de código C a seguir demonstra como você usaria o OpenSSL para derivar o material principal:

/* 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);

}

Etapa 2: Criptografar e descriptografar mensagens

Configure a criptografia na API do terminal para proteger as mensagens comunicadas entre o terminal e a caixa registradora.

Criptografar mensagens

Para criptografar uma mensagem:

  1. Criptografar o corpo da mensagem usando um vetor de inicialização dk.iv (IV) do Nonce XOR, em que dk.iv é o IV no material da chave derivada.
    O nonce (número usado uma vez) é uma sequência aleatória de bytes do mesmo tamanho que dk.iv gerado por mensagem.
  2. Coloque o nonce na SecurityTrailer como uma cadeia de caracteres codificada Base64.

Descriptografar e validar mensagens

Para descriptografar e validar uma mensagem:

  1. Decomponha a mensagem em suas várias partes.

  2. Verifique o AdyenCryptoVersion no SecurityTrailer.

  3. Recupere o material principal com base em KeyIdentifier e KeyVersion.

  4. Descriptografe o blob usando o IV e digite o material da chave e o Nonce no SecurityTrailer.

  5. Valide o HMAC computando-o no blob descriptografado.

    Não valide no formulário serializado.

  6. Compare o resultado ao blob no SecurityTrailer. A mensagem contém uma cópia em texto não criptografado MessageHeader para fins de roteamento.

  7. Uma vez emparelhado, compare o texto não criptografado MessageHeadercom oMessageHeaderdo corpo descriptografado e verifique se eles não são iguais aosMessageHeader` da parte descriptografado.

Configure uma chave na área do cliente

A chave que o terminal utiliza, recuperada com a configuração do terminal, é definida na Customer Area. Atualmente, o software do terminal é capaz de usar apenas uma chave. Se vários dispositivos estiverem se comunicando com o mesmo terminal, eles deverão compartilhar a mesma chave.

Para configurar a chave:

  1. Abra a Customer Area e vá para Point-of-sale > Terminals.
  2. Clique na linha do terminal que você deseja configurar. A página de configuração desse terminal será aberta.
  3. Clique em View decrypted properties.
    A página é atualizada e retorna ao menu da API do terminal.
  4. Clique em Terminal API tab.
  5. Clique em Edit.
  6. Digite o identificador da chave de criptografia e a senha da chave.
  7. Clique em Save.
  8. Adicione sua chave no campo Security Keys.

A seguir, é apresentado um exemplo da propriedade das Chaves de Segurança:

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

Exemplo de criptografia

O código de exemplo abaixo implementa criptografia de mensagens, computação e construção do HMAC, além da operação reversa: divisão, validação e descriptografia do HMAC.

Por padrão, o Java tem restrição AES 128. Pode ser necessário fazer o download de um novo arquivo de políticas para ativar o AES 256, caso contrário, a exceção de chave ilegal será lançada.

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;
   }

}

Veja também