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