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 o certificado do terminal. Isso confirma que sua caixa registradora está se comunicando diretamente com um terminal da Adyen e não com um impostor.
- Criptografar comunicações . Isso impede que os invasores leiam as mensagens transmitidas entre a caixa registradora e o terminal.
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:
- Instalar o certificado raiz público da Adyen trust store.
- 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:
-
Faça o download dos certificados raiz públicos da Adyen em sua caixa registradora:
- Certificado do terminal TEST: Para validar o certificado nos terminais Adyen TEST.
- Certificado do terminal LIVE: FPara validar o certificado nos terminais Adyen LIVE.
Esses certificados podem ser convertidos para formatos como .crt, .cer, ou .p12, se necessário.
-
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
- Certificado de terminal TEST:
-
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:
-
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.
-
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:
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:
- Compartilhar um segredo de comprimento variável para obter o material principal.
- Derivar a chave usando PBKDF2_HMAC_SHA1
-
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:
- 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. - Coloque o nonce na
SecurityTrailer
como uma cadeia de caracteres codificada Base64.
Descriptografar e validar mensagens
Para descriptografar e validar uma mensagem:
-
Decomponha a mensagem em suas várias partes.
-
Verifique o
AdyenCryptoVersion
noSecurityTrailer
. -
Recupere o material principal com base em
KeyIdentifier
eKeyVersion
. -
Descriptografe o blob usando o IV e digite o material da chave e o Nonce no
SecurityTrailer
. -
Valide o HMAC computando-o no blob descriptografado.
Não valide no formulário serializado.
-
Compare o resultado ao blob no
SecurityTrailer
. A mensagem contém uma cópia em texto não criptografadoMessageHeader
para fins de roteamento. -
Uma vez emparelhado, compare o texto não criptografado MessageHeader
com o
MessageHeaderdo corpo descriptografado e verifique se eles não são iguais aos
MessageHeader` 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:
- Abra a Customer Area e vá para In-person payments > Terminals.
- Clique na linha do terminal que você deseja configurar. A página de configuração desse terminal será aberta.
- Clique em View decrypted properties.
A página é atualizada e retorna ao menu da API do terminal. - Clique em Terminal API tab.
- Clique em Edit.
- Digite o identificador da chave de criptografia e a senha da chave.
- Clique em Save.
- 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; } }