JavaScript
This guide shows you how to request and decrypt sensitive card details using Node.js. 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:
- Node.js 18 or later installed on your system.
Note
The code samples use the global fetch, which is available by default starting in Node.js 18.
- 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 Node.js's built-in crypto module. Keep the private key in scope to decrypt the response:
import crypto from "crypto";
const { privateKey, publicKey } = crypto.generateKeyPairSync("rsa", {
modulusLength: 2048,
});
Encode the public key
Export the public key as PEM (SubjectPublicKeyInfo), then Base64-encode the entire PEM string. This is the value to send in the X-Client-Public-Key header:
const publicKeyPem = publicKey.export({ type: "spki", format: "pem" });
const base64PublicKey = Buffer.from(publicKeyPem, "utf8").toString("base64");
Caution
Don't send only the modulus (N field of the key), the raw DER bytes, or the PEM bytes without Base64-encoding them. 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:
const response = await fetch(
`https://api.gravv.xyz/v1/cards/${cardId}/sensitive-details`,
{
method: "GET",
headers: {
"Api-Key": apiKey,
"X-Client-Public-Key": base64PublicKey,
"Content-Type": "application/json",
},
}
);
const { data: payload, error } = await response.json();
if (error) {
throw new Error(`Gravv API error: ${JSON.stringify(error)}`);
}
The payload contains the encrypted_key, nonce, and ciphertext fields needed to decrypt the card details. The remaining steps use the in-memory privateKey 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 |
Decode the Base64 values
Convert all three Base64-encoded values from the API response into buffers:
const encryptedKey = Buffer.from(payload.encrypted_key, "base64");
const nonce = Buffer.from(payload.nonce, "base64");
const ciphertext = Buffer.from(payload.ciphertext, "base64");
Decrypt the AES key
Use your RSA private key to decrypt the AES key. The decryption uses RSA-OAEP padding with SHA-256. The key option accepts either a crypto.KeyObject (such as the in-memory privateKey generated above) or a PEM-encoded string:
const aesKey = crypto.privateDecrypt(
{
key: privateKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: "sha256",
},
encryptedKey
);
Separate the authentication tag
The last 16 bytes of the ciphertext contain the GCM authentication tag. Separate this from the encrypted data:
const authTag = ciphertext.slice(ciphertext.length - 16);
const encryptedData = ciphertext.slice(0, ciphertext.length - 16);
Decrypt the card details
Create an AES-256-GCM decipher, set the authentication tag, and decrypt the card details:
const decipher = crypto.createDecipheriv("aes-256-gcm", aesKey, nonce);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encryptedData).toString("utf8");
decrypted += decipher.final("utf8");
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 crypto from "crypto";
export async function getCardSensitiveDetails({ apiKey, cardId, baseUrl = "https://api.gravv.xyz" }) {
// Step 1: Generate RSA key pair in memory
const { privateKey, publicKey } = crypto.generateKeyPairSync("rsa", {
modulusLength: 2048,
});
// Step 2: Base64-encode the PEM-formatted public key
const publicKeyPem = publicKey.export({ type: "spki", format: "pem" });
const base64PublicKey = Buffer.from(publicKeyPem, "utf8").toString("base64");
// Step 3: Request the encrypted card details
const response = await fetch(`${baseUrl}/v1/cards/${cardId}/sensitive-details`, {
method: "GET",
headers: {
"Api-Key": apiKey,
"X-Client-Public-Key": base64PublicKey,
"Content-Type": "application/json",
},
});
const body = await response.json();
if (!response.ok || body.error) {
throw new Error(`Gravv API error: ${JSON.stringify(body.error ?? body)}`);
}
const payload = body.data;
if (!payload?.encrypted_key || !payload?.ciphertext || !payload?.nonce) {
throw new Error("Card sensitive details are not available. Please try again in a moment.");
}
// Step 4: Decode Base64 values
const encryptedKey = Buffer.from(payload.encrypted_key, "base64");
const nonce = Buffer.from(payload.nonce, "base64");
const ciphertext = Buffer.from(payload.ciphertext, "base64");
// Step 5: Decrypt the AES key using RSA-OAEP
const aesKey = crypto.privateDecrypt(
{
key: privateKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: "sha256",
},
encryptedKey
);
// Step 6: Separate authentication tag from ciphertext
const authTag = ciphertext.slice(ciphertext.length - 16);
const encryptedData = ciphertext.slice(0, ciphertext.length - 16);
// Step 7: Decrypt using AES-256-GCM
const decipher = crypto.createDecipheriv("aes-256-gcm", aesKey, nonce);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encryptedData, null, "utf8");
decrypted += decipher.final("utf8");
return JSON.parse(decrypted);
}
Usage example
The following example calls getCardSensitiveDetails with an API key and card ID loaded from environment variables and logs the decrypted card details to the console:
const cardDetails = await getCardSensitiveDetails({
apiKey: process.env.GRAVV_API_KEY,
cardId: "dce0192a-9b3d-440b-9500-33dc9ac8dc20",
});
console.log("Card details:", cardDetails);
// { pan: "4111111111111111", cvv: "123" }
If you already have the encrypted payload and only need to decrypt it, call crypto.privateDecrypt and crypto.createDecipheriv as shown in the Decrypt the AES key and Decrypt the card details sections with the PEM string in the key option.