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.