Skip to content

Golang

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

  • 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 using Go's crypto/rsa package. Keep the private key in scope 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)

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:

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 needed to decrypt the card details. The subsequent steps use the same in-memory privateKey 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 + 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 automatically validates the 16-byte authentication tag, which the server 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

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:

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

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