Golang

Request and decrypt a card's full number and CVV with Go. The same decryption works for the PIN.

This guide shows how to request and decrypt sensitive card details with Go. 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:

  • Go 1.21 or later installed on your system
  • 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 Go's crypto/rsa package. Keep the private key in scope, because you need it to decrypt the response:

import (
    "crypto/rand"
    "crypto/rsa"
    "fmt"
)

privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
    return nil, fmt.Errorf("generate rsa key: %w", err)
}

Encode the public key

Marshal the public key as DER SubjectPublicKeyInfo, wrap it in a PEM block, then Base64-encode the PEM bytes. This is the value to send in the X-Client-Public-Key header:

import (
    "crypto/x509"
    "encoding/base64"
    "encoding/pem"
    "fmt"
)

publicKeyDER, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey)
if err != nil {
    return nil, fmt.Errorf("marshal public key: %w", err)
}

publicKeyPEM := pem.EncodeToMemory(&pem.Block{
    Type:  "PUBLIC KEY",
    Bytes: publicKeyDER,
})

base64PublicKey := base64.StdEncoding.EncodeToString(publicKeyPEM)

Do not 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. 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 (
    "encoding/json"
    "fmt"
    "net/http"
)

req, err := http.NewRequest(
    http.MethodGet,
    fmt.Sprintf("https://api.gravv.xyz/v1/cards/%s/sensitive-details", cardID),
    nil,
)
if err != nil {
    return nil, fmt.Errorf("build request: %w", err)
}
req.Header.Set("Api-Key", apiKey)
req.Header.Set("X-Client-Public-Key", base64PublicKey)
req.Header.Set("Content-Type", "application/json")

resp, err := http.DefaultClient.Do(req)
if err != nil {
    return nil, fmt.Errorf("send request: %w", err)
}
defer resp.Body.Close()

var body struct {
    Data  map[string]string `json:"data"`
    Error any               `json:"error"`
}
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
    return nil, fmt.Errorf("decode response: %w", err)
}
if body.Error != nil {
    return nil, fmt.Errorf("gravv api error: %v", body.Error)
}

The body.Data map contains the encrypted_key, nonce, and ciphertext fields that you need to decrypt the card details. The next steps use the same in-memory privateKey from 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 byte slices:

encryptedKey, err := base64.StdEncoding.DecodeString(body.Data["encrypted_key"])
nonce, err := base64.StdEncoding.DecodeString(body.Data["nonce"])
ciphertext, err := base64.StdEncoding.DecodeString(body.Data["ciphertext"])

Decrypt the AES key

Use your RSA private key to decrypt the AES key. The decryption uses RSA-OAEP with SHA-256:

import (
    "crypto/rand"
    "crypto/rsa"
    "crypto/sha256"
    "fmt"
)

aesKey, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, privateKey, encryptedKey, nil)
if err != nil {
    return nil, fmt.Errorf("rsa decrypt aes key: %w", err)
}

Decrypt the card details

Create an AES-256-GCM cipher and decrypt the card details. gcm.Open checks the 16-byte authentication tag that Gravv appends to the ciphertext:

import (
    "crypto/aes"
    "crypto/cipher"
    "errors"
    "fmt"
)

block, err := aes.NewCipher(aesKey)
if err != nil {
    return nil, fmt.Errorf("aes cipher: %w", err)
}

gcm, err := cipher.NewGCM(block)
if err != nil {
    return nil, fmt.Errorf("aes-gcm: %w", err)
}

if len(nonce) != gcm.NonceSize() {
    return nil, errors.New("invalid nonce size")
}

plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
    return nil, fmt.Errorf("gcm decrypt: %w", err)
}

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:

package gravv

import (
    "crypto/aes"
    "crypto/cipher"
    "crypto/rand"
    "crypto/rsa"
    "crypto/sha256"
    "crypto/x509"
    "encoding/base64"
    "encoding/json"
    "encoding/pem"
    "errors"
    "fmt"
    "net/http"
)

type CardDetails struct {
    PAN string `json:"pan"`
    CVV string `json:"cvv"`
}

func GetCardSensitiveDetails(apiKey, cardID, baseURL string) (*CardDetails, error) {
    if baseURL == "" {
        baseURL = "https://api.gravv.xyz"
    }

    // Step 1: Generate RSA key pair in memory
    privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
    if err != nil {
        return nil, fmt.Errorf("generate rsa key: %w", err)
    }

    // Step 2: Base64-encode the PEM-formatted public key
    publicKeyDER, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey)
    if err != nil {
        return nil, fmt.Errorf("marshal public key: %w", err)
    }
    publicKeyPEM := pem.EncodeToMemory(&pem.Block{
        Type:  "PUBLIC KEY",
        Bytes: publicKeyDER,
    })
    base64PublicKey := base64.StdEncoding.EncodeToString(publicKeyPEM)

    // Step 3: Request the encrypted card details
    req, err := http.NewRequest(
        http.MethodGet,
        fmt.Sprintf("%s/v1/cards/%s/sensitive-details", baseURL, cardID),
        nil,
    )
    if err != nil {
        return nil, fmt.Errorf("build request: %w", err)
    }
    req.Header.Set("Api-Key", apiKey)
    req.Header.Set("X-Client-Public-Key", base64PublicKey)
    req.Header.Set("Content-Type", "application/json")

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, fmt.Errorf("send request: %w", err)
    }
    defer resp.Body.Close()

    var body struct {
        Data  map[string]string `json:"data"`
        Error any               `json:"error"`
    }
    if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
        return nil, fmt.Errorf("decode response: %w", err)
    }
    if body.Error != nil {
        return nil, fmt.Errorf("gravv api error: %v", body.Error)
    }
    if body.Data["encrypted_key"] == "" || body.Data["nonce"] == "" || body.Data["ciphertext"] == "" {
        return nil, errors.New("card sensitive details are not available. Please try again in a moment")
    }

    // Step 4: Decode Base64 values
    encryptedKey, err := base64.StdEncoding.DecodeString(body.Data["encrypted_key"])
    if err != nil {
        return nil, fmt.Errorf("decode encrypted_key: %w", err)
    }
    nonce, err := base64.StdEncoding.DecodeString(body.Data["nonce"])
    if err != nil {
        return nil, fmt.Errorf("decode nonce: %w", err)
    }
    ciphertext, err := base64.StdEncoding.DecodeString(body.Data["ciphertext"])
    if err != nil {
        return nil, fmt.Errorf("decode ciphertext: %w", err)
    }

    // Step 5: Decrypt the AES key using RSA-OAEP
    aesKey, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, privateKey, encryptedKey, nil)
    if err != nil {
        return nil, fmt.Errorf("rsa decrypt aes key: %w", err)
    }

    // Step 6: Decrypt the card details using AES-256-GCM
    block, err := aes.NewCipher(aesKey)
    if err != nil {
        return nil, fmt.Errorf("aes cipher: %w", err)
    }
    gcm, err := cipher.NewGCM(block)
    if err != nil {
        return nil, fmt.Errorf("aes-gcm: %w", err)
    }
    if len(nonce) != gcm.NonceSize() {
        return nil, errors.New("invalid nonce size")
    }
    plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
    if err != nil {
        return nil, fmt.Errorf("gcm decrypt: %w", err)
    }

    var details CardDetails
    if err := json.Unmarshal(plaintext, &details); err != nil {
        return nil, fmt.Errorf("unmarshal card details: %w", err)
    }
    return &details, nil
}

Usage example

This example calls GetCardSensitiveDetails with the API key from the GRAVV_API_KEY environment variable and a sample card ID, and prints the decrypted card details:

package main

import (
    "fmt"
    "log"
    "os"

    "example.com/gravv"
)

func main() {
    details, err := gravv.GetCardSensitiveDetails(
        os.Getenv("GRAVV_API_KEY"),
        "dce0192a-9b3d-440b-9500-33dc9ac8dc20",
        "",
    )
    if err != nil {
        log.Fatalf("get card details: %v", err)
    }

    fmt.Printf("Card details: pan=%s cvv=%s\n", details.PAN, details.CVV)
}

If you already have the encrypted payload and only need to decrypt it, parse your existing PEM private key with x509.ParsePKCS1PrivateKey (PKCS#1) or x509.ParsePKCS8PrivateKey (PKCS#8) and reuse the Decrypt the AES key and Decrypt the card details snippets.

Was this page helpful?