Python
Request and decrypt a card's full number and CVV with Python. The same decryption works for the PIN.
This guide shows how to request and decrypt sensitive card details with Python. The code generates an RSA key pair in memory, Base64-encodes the public key in PEM format, and sends it in the X-Client-Public-Key header. To decrypt the response, it unwraps the Advanced Encryption Standard (AES) key with Rivest-Shamir-Adleman Optimal Asymmetric Encryption Padding (RSA-OAEP). It then decrypts the card data with Advanced Encryption Standard 256-bit Galois/Counter Mode (AES-256-GCM).
Prerequisites
Before you begin, you need:
-
Python 3.8 or later installed on your system
-
The
cryptographyandrequestslibraries installed:pip install cryptography requests -
An API key for the Gravv API
-
A card ID to retrieve sensitive details for
Generate the RSA key pair
Generate a 2048-bit RSA key pair in memory with the cryptography library. Keep the private key in scope, because you need it to decrypt the response:
from cryptography.hazmat.primitives.asymmetric import rsa
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
)
public_key = private_key.public_key()
Encode the public key
Serialize the public key to PEM (SubjectPublicKeyInfo), then Base64-encode the entire PEM byte string. This is the value to send in the X-Client-Public-Key header:
import base64
from cryptography.hazmat.primitives import serialization
public_key_pem = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
base64_public_key = base64.b64encode(public_key_pem).decode("ascii")
Do not send only the modulus (
nfrom the key), the raw DER bytes, or the PEM string without Base64-encoding it. The header value must Base64-decode to the full PEM text, including the-----BEGIN PUBLIC KEY-----and-----END PUBLIC KEY-----markers. Gravv rejects a header that contains newlines or spaces, so you cannot send raw PEM directly.
Request the encrypted card details
Send a GET request to the View card sensitive details endpoint with the Base64-encoded public key:
import requests
response = requests.get(
f"https://api.gravv.xyz/v1/cards/{card_id}/sensitive-details",
headers={
"Api-Key": api_key,
"X-Client-Public-Key": base64_public_key,
"Content-Type": "application/json",
},
)
response.raise_for_status()
body = response.json()
if body.get("error"):
raise RuntimeError(f"Gravv API error: {body['error']}")
payload = body["data"]
The payload contains the encrypted_key, nonce, and ciphertext fields that you need to decrypt the card details. The next steps use the in-memory private_key that you generated in Generate the RSA key pair.
Understanding the encrypted response
The View card sensitive details and the Get card PIN endpoints return three Base64-encoded values:
| Field | Description |
|---|---|
| encrypted_key | AES-256 key encrypted with your RSA public key |
| nonce | 12-byte initialization vector for AES-GCM decryption |
| ciphertext | Encrypted card details (AES-GCM ciphertext followed by the 16-byte authentication tag) |
Decode the Base64 values
Convert all three Base64-encoded values from the API response into bytes:
encrypted_key = base64.b64decode(payload["encrypted_key"])
nonce = base64.b64decode(payload["nonce"])
ciphertext = base64.b64decode(payload["ciphertext"])
Decrypt the AES key
Use your RSA private key to decrypt the AES key. The decryption uses RSA-OAEP padding with SHA-256:
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
aes_key = private_key.decrypt(
encrypted_key,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None,
),
)
Decrypt the card details
Create an AES-GCM cipher and decrypt the card details. The AESGCM.decrypt method checks the authentication tag that Gravv appends to the ciphertext:
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
aesgcm = AESGCM(aes_key)
plaintext = aesgcm.decrypt(nonce, ciphertext, None)
card_details = plaintext.decode("utf-8")
Complete end-to-end code
This code generates an RSA key pair, encodes the public key, requests sensitive card details and decrypts the response in one function. It holds the private key only in memory:
import base64
import json
import requests
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
def get_card_sensitive_details(
api_key: str,
card_id: str,
base_url: str = "https://api.gravv.xyz",
) -> dict:
# Step 1: Generate RSA key pair in memory
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
)
# Step 2: Base64-encode the PEM-formatted public key
public_key_pem = private_key.public_key().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
base64_public_key = base64.b64encode(public_key_pem).decode("ascii")
# Step 3: Request the encrypted card details
response = requests.get(
f"{base_url}/v1/cards/{card_id}/sensitive-details",
headers={
"Api-Key": api_key,
"X-Client-Public-Key": base64_public_key,
"Content-Type": "application/json",
},
)
response.raise_for_status()
body = response.json()
if body.get("error"):
raise RuntimeError(f"Gravv API error: {body['error']}")
payload = body.get("data") or {}
if not all(k in payload for k in ("encrypted_key", "nonce", "ciphertext")):
raise RuntimeError(
"Card sensitive details are not available. Please try again in a moment."
)
# Step 4: Decode Base64 values
encrypted_key = base64.b64decode(payload["encrypted_key"])
nonce = base64.b64decode(payload["nonce"])
ciphertext = base64.b64decode(payload["ciphertext"])
# Step 5: Decrypt the AES key using RSA-OAEP
aes_key = private_key.decrypt(
encrypted_key,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None,
),
)
# Step 6: Decrypt the card details using AES-256-GCM
aesgcm = AESGCM(aes_key)
plaintext = aesgcm.decrypt(nonce, ciphertext, None)
return json.loads(plaintext.decode("utf-8"))
Usage example
This example calls get_card_sensitive_details with the API key from the GRAVV_API_KEY environment variable and a sample card ID, and prints the decrypted card details:
import os
card_details = get_card_sensitive_details(
api_key=os.environ["GRAVV_API_KEY"],
card_id="dce0192a-9b3d-440b-9500-33dc9ac8dc20",
)
print("Card details:", card_details)
# {"pan": "4111111111111111", "cvv": "123"}
If you already have the encrypted payload and only need to decrypt it, load your existing private key with serialization.load_pem_private_key(priv_pem, password=None) and reuse the Decrypt the AES key and Decrypt the card details snippets.