RSA 2048 y AES 256: cómo encriptar los datos de tarjeta antes de enviarlos.
Encriptar los datos de tarjeta antes de enviarlos es opcional, pero recomendado.
Los endpoints que reciben card_information aceptan tres formas de request:
- El body completo en claro.
- El body en claro con
card_informationencriptado. - El body completo encriptado.
En los dos últimos casos debes enviar además el parámetro encryption_key: es el identificador con el que Toku guardó la llave privada al generarla.
Para el body completo, usa AESRSA 2048 con PKCS1_OAEP cifra como máximo 214 bytes por operación. Un
card_informationcabe; un body completo —conid_customer,id_account,id_invoicesy la tarjeta— no, y la librería falla conValueError: Plaintext is too long. Encripta el body completo con AES, o limítate a encriptarcard_informationsi vas a usar RSA.
Protocolo de encriptación
Soportamos dos algoritmos:
- RSA 2048 con PKCS1_OAEP
- AES 256 en modo CBC (Cipher-Block Chaining)
Para saber más:
- https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html
- https://pycryptodome.readthedocs.io/en/latest/src/cipher/classic.html#cbc-mode
Cómo encriptar la data
RSA
Usa el public_key que devuelve Generar una llave pública de encriptación.
import base64
import json
from Crypto.Cipher import PKCS1_OAEP
from Crypto.PublicKey import RSA
def encrypt_data(public_key: bytes, data: dict) -> str:
"""Encrypts the provided data in json format using the provided public key"""
str_data = json.dumps(data)
key = RSA.import_key(public_key)
cipher = PKCS1_OAEP.new(key)
encrypted_data = cipher.encrypt(str_data.encode("utf-8"))
return base64.b64encode(encrypted_data).decode("utf-8")Ejemplo de uso
Un cobro único con los datos de tarjeta encriptados con RSA, de principio a fin:
import base64
import json
import uuid
import requests
from Crypto.Cipher import PKCS1_OAEP
from Crypto.PublicKey import RSA
X_ORG_ID = "<X-ORG-ID>"
X_API_KEY = "<SECURE-API-KEY>"
ID_ACCOUNT = "<ID-ACCOUNT>"
ENCRYPTION_KEY = str(uuid.uuid4()) # este valor tiene que ser único
SECURE_API_URL = "https://secure.api.trytoku.com"
def encrypt_data(public_key: bytes, data: dict) -> str:
"""Encrypts the provided data in json format using the provided public key"""
str_data = json.dumps(data)
key = RSA.import_key(public_key)
cipher = PKCS1_OAEP.new(key)
encoded_data = str_data.encode("utf-8")
encrypted_data = cipher.encrypt(encoded_data)
return base64.b64encode(encrypted_data).decode("utf-8")
def create_public_key(encryption_key: str) -> str:
"""Create the public key using the encryption_key"""
url = f"{SECURE_API_URL}/public-encryption-keys?encryption_key={encryption_key}"
headers = {"x-org-id": X_ORG_ID, "x-api-key": X_API_KEY}
response = requests.post(url, headers=headers)
response.raise_for_status()
return response.json()["public_key"]
def send_one_time_payment(payload: dict) -> dict:
"""Send request to make a one time payment"""
url = f"{SECURE_API_URL}/cards/onetime/entry-point"
headers = {
"x-org-id": X_ORG_ID,
"x-api-key": X_API_KEY,
"Content-Type": "application/json",
}
response = requests.post(url, headers=headers, json=payload)
response.raise_for_status()
return response.json()
payload = {
"id_customer": "<ID-CUSTOMER>",
"toku_product": "payment_orchestration_onetime",
"id_account": ID_ACCOUNT,
"transaction_external_id": "<TRANSACTION-EXTERNAL-ID>",
"engine": "SAAS",
"id_invoices": ["<ID-INVOICE>"],
"encryption_key": ENCRYPTION_KEY,
}
CARD_INFORMATION = {
"number": "<CARD-NUMBER>",
"verification_value": "<CVV>",
"month": "<MONTH>",
"year": "<YEAR>",
"full_name": "<CARD-HOLDER-NAME>",
}
public_key = create_public_key(ENCRYPTION_KEY)
payload["card_information"] = encrypt_data(public_key.encode(), CARD_INFORMATION)
send_one_time_payment(payload)AES
Usa la secret_key que devuelve Generar una llave pública de encriptación. En AES la misma llave encripta y desencripta.
La secret_key viaja en base64: decodifícala antes de pasarla al cifrador, o AES la recibe como 44 caracteres en vez de 32 bytes y falla con Incorrect AES key length.
import base64
import json
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
def encrypt_data(secret_key_b64: str, data: dict) -> str:
"""Encrypts the provided data in JSON format using AES-256 (CBC mode)"""
secret_key = base64.b64decode(secret_key_b64)
str_data = json.dumps(data)
cipher = AES.new(secret_key, AES.MODE_CBC)
ct_bytes = cipher.encrypt(pad(str_data.encode("utf-8"), AES.block_size))
iv_bytes = bytes(cipher.iv)
combined = iv_bytes + ct_bytes
return base64.b64encode(combined).decode("utf-8")
El IV va por delanteEl ciphertext que envías es
IV + ciphertexten base64, en ese orden. Toku separa los primeros 16 bytes como IV para desencriptar.