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 cryptography and requests libraries 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 (n from 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.

Was this page helpful?