Skip to content

Python

This guide shows you how to request and decrypt sensitive card details using Python. The request flow 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. The decryption flow uses Rivest-Shamir-Adleman Optimal Asymmetric Encryption Padding (RSA-OAEP) to unwrap the Advanced Encryption Standard (AES) key. Then Advanced Encryption Standard 256-bit Galois/Counter Mode (AES-256-GCM) is used to decrypt the card data.

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 using the cryptography library. Keep the private key in scope 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")

Caution

Don't 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. The header is rejected if it contains newlines or spaces, which is why raw PEM cannot be sent 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 needed to decrypt the card details. The remaining steps use the in-memory private_key you generated above.

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 + 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 automatically validates the authentication tag, which the server 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

The following implementation generates an RSA key pair, encodes the public key, requests sensitive card details, and decrypts the response in a single function. The private key is held 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

The following example calls get_card_sensitive_details with an API key and card ID loaded from environment variables 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.