Retrieve sensitive card details

Show the full card number, CVV and PIN on the cardholder's device only, encrypted to a key the device holds.

This page shows how to read a card's full number, CVV and PIN so that only the cardholder's device can decrypt them. Gravv encrypts these sensitive card details (the card number pan, the cvv and the card PIN) with a one-time AES-256-GCM key. It then wraps that key with your Rivest-Shamir-Adleman (RSA) public key, using RSA-OAEP with SHA-256 (MGF1-SHA-256, no label).

A money example. No money moves. Ada taps "show details" in your app to pay 9.99 USD for a subscription online. Her phone makes a key pair. Your server asks for the details with the phone's public key. Only her phone can read the number and CVV.

Make the key pair on the cardholder's device (or another client you control), and keep the private key there. Your server only relays the encrypted response. Do not log it. Use an RSA key of 2048 bits or more. These routes need no special key permission, so limit who can call them in your own backend.

Generate RSA key pair

Generate an RSA key pair with OpenSSL. The pair has a private key and a public key:

openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -pubout -out public.pem

This creates two files:

private.pem Keep this secure on your server and never transmit it
public.pem You encode this and send it to Gravv in the API request header

Encode the public key

Base64-encode the entire public.pem file, including the -----BEGIN PUBLIC KEY----- and -----END PUBLIC KEY----- markers and the newlines between them. The result is a single-line base64 string, which you send in the API request header.

# Linux
base64 -w 0 public.pem > public.pem.b64

# macOS
base64 -i public.pem -o public.pem.b64

When Gravv base64-decodes the header value, the result must be the original PEM text. If you encode only the modulus, the DER bytes or any other part of the key, the request fails.

Request encrypted card details

Send the base64-encoded PEM public key in the X-Client-Public-Key header when you request sensitive card details:

curl --request GET \
     --url https://api.gravv.xyz/v1/cards/a14ec356-f33d-4384-ba68-4d9bfa19765b/sensitive-details \
     --header 'Api-Key: <API_KEY>' \
     --header 'X-Client-Public-Key: LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUEuLi4tLS0tLUVORCBQVUJMSUMgS0VZLS0tLS0='

Replace the header value with the full base64-encoded contents of your public.pem.

The response contains the encrypted card details:

{
  "data": {
    "ciphertext": "IjBkZlJMWUVXMzJ5MjdySzhWQlp1WHM5N1hzZ0szNGFSb1VScTJrbDhyazkzSmUrM0lTTXNiOCs4bmo2MW9LdDYxdkN0d0w1NSI=",
    "encrypted_key": "IkUxdXhGa2c3ekpVaUJmWFlhcnVhM0dZRyt1amZJczBpMDJsak85QnIvVjkzSGxvWWxDNk1xaVlyYkNGQnJtTHFkVkwybmlFbkxDemliOUoweWI3RTg5YUFvTEZncmFkNFV0Z2cvV1ZUK29HbGZPZWRJVnREU1ZvSmNWaDMrSExnSUMybnBaTWJkelpualB6b21PcThCcGpNN09yWG1FeHUvd3pHblhpYXIyRT0i",
    "nonce": "Img2M29JSVVESTZ2aS90S2si"
  },
  "error": null
}

Decrypt sensitive card details

To decrypt the response:

  1. Unwrap encrypted_key with your RSA private key (RSA-OAEP, SHA-256). The result is the AES key.
  2. Decrypt ciphertext with that AES key and nonce (AES-256-GCM). The 16-byte tag is at the end of the ciphertext.
  3. Parse the plaintext, which is JSON: {"pan": "…", "cvv": "…"}. For the PIN endpoint, it is {"pin": "…"}.

The plaintext has no expiry. Read expiry from Get card.

If decryption fails on the device, check that the library uses SHA-256 for OAEP. Many libraries default to SHA-1.

These guides have complete decryption code for each language:

Decrypt card details with Go's crypto package, using RSA-OAEP and AES-GCM.

Decrypt card details with Python's cryptography library.

Decrypt card details with the Node.js crypto module, using RSA and AES-GCM.

What can go wrong

Response Why Fix
400 x_client_public_key is required The header is missing Send X-Client-Public-Key
400 x_client_public_key must be a base64-encoded RSA public key in PEM format: the header value is not valid base64 The header is URL-safe base64, or the raw PEM Send standard base64 of the whole PEM text
400 parse public key: x_client_public_key must be a base64-encoded RSA public key in PEM format: no PEM block found The header is base64 of the DER bytes Encode the PEM text, with its header lines
400 internal server error The key is too small for RSA-OAEP with SHA-256 Use 2048 bits or more
400 card does not exist, whatever key you send The API checks the card id before the header, so a wrong id hides every key error Check the card id first
400 card sensitive details missing The card is not issued yet, or the details cannot be revealed right now Check that the card is issued, then retry later
Was this page helpful?