Tokenize cards with backend side encryption

Backend Side Encryption payment details to be sent directly to Payrails in order to tokenize a Payment Instrument.

Credit card details are encrypted on the backend side with a public key given by Payrails. The encrypted data is then sent to Payrails and is then tokenized and returned to the client as a Payment Instrument.

Payrails Backend Side Encryption uses JWE with encryption algorithm RSA-OAEP-256 and content encryption A256CBC-HS512.

What is tokenization?

Tokenization refers to the process of collecting sensitive payment information directly and returning a short-term, single-use token that represents this information. During this process, Payrails handles the sensitive payment information and assume responsibility for PCI compliance. You then use that token to request a payment using the Unified Payments API.

How it works

  1. You collect the payment details from your customer
  2. You encrypt the payment details with a public key given by Payrails
  3. You send the encrypted data from your backend to the tokenization endpoint of Payrails
  4. Payrails returns a tokenized Payment Instrument

How to use tokenize instruments with Backend Side Encryption

Here's a tokenization flow:

You can implement encryption in 4 steps:

Step 1. Collect the payment details from the user

Collect and arrange all the relevant payment details into a json object with the following fields (securityCode is optional):

{
    "cardNumber": "4111111111111111",
    "expiryMonth": "03",
    "expiryYear": "30",
    "securityCode": "737",
    "holderName": "John Doe",
    "holderReference": "customer123"
}

Step 2. Encrypt the card data in your backend

Encrypt the full json of the payment details with the public key given by Payrails:

  • The public key is a PKCS8 RSA public key in PEM format without header and line breaks. The key will be given to you by Payrails.
  • The encrypted data should be encrypted using JWE with encryption algorithm RSA-OAEP-256 and content encryption A256CBC-HS512.

Examples in on how to encrypt payment details with the public key given by Payrails:

package main

import (
	"encoding/json"
	"fmt"

	"github.com/golang-module/dongle/openssl"
	"gopkg.in/square/go-jose.v2"
)

func main() {
	instrumentDetails := InstrumentDetails{
    HolderReference: "customer123",
		HolderName:      "John Doe",
		CardNumber:      "4111111111111111",
		ExpiryMonth:     "03",
		ExpiryYear:      "30",
		SecurityCode:    "737",
	}

	instrumentDetailsJSON, err := json.Marshal(instrumentDetails)
	if err != nil {
		// handle error
		panic(err)
	}
	fmt.Println(string(instrumentDetailsJSON))
	// {"cardNumber":"4111111111111111","expiryMonth":"03","expiryYear":"30","securityCode":"737","holderName":"John Doe","holderReference":"customer123"}

	publicKey := "MIIBCgKCAQEAuJeo7zyuzdBx8biauFhVpy9XXk4lgvOe1/xD2G..."
	encryptedCardData, err := jweEncrypt(publicKey, instrumentDetailsJSON)
	if err != nil {
		// handle error
		panic(err)
	}
	fmt.Println(encryptedCardData)
	// eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIn0...
}

// InstrumentDetails represents the instrument details to be encrypted
type InstrumentDetails struct {
	CardNumber   string    `json:"cardNumber"`
	ExpiryMonth  string    `json:"expiryMonth"`
	ExpiryYear   string    `json:"expiryYear"`
	SecurityCode string    `json:"securityCode,omitempty"`
	HolderName   string    `json:"holderName"`
  HolderReference string `json:"holderReference"`
}

func jweEncrypt(publicKeyStr string, jsonData []byte) (string, error) {
	publicKey, err := openssl.RSA.ParsePublicKey(openssl.RSA.FormatPublicKey(openssl.PKCS8, []byte(publicKeyStr)))
	if err != nil {
		return "", err
	}
	recipient := jose.Recipient{
		Algorithm: jose.RSA_OAEP_256,
		Key:       publicKey,
		KeyID:     "",
	}

	e, err := jose.NewEncrypter(jose.A256CBC_HS512, recipient, nil)
	if err != nil {
		return "", err
	}

	encrypted, err := e.Encrypt(jsonData)
	if err != nil {
		return "", err
	}
	return encrypted.CompactSerialize()
}
# Due to an issue in python-jose (https://github.com/mpdavis/python-jose/issues/281)
# Please use the fork which allows to use RSA-OAEP-256: https://github.com/jkamp-aws/python-jose
# Ex:
# pip3 install cryptograph
# pip3 install git+https://github.com/jkamp-aws/python-jose

from jose import jwe
import json

def format_public_key_to_pem(public_key):
    pem_header = "-----BEGIN PUBLIC KEY-----"
    pem_footer = "-----END PUBLIC KEY-----"

    chunks = [public_key[i:i+64] for i in range(0, len(public_key), 64)]
    pem_content = "\n".join(chunks)
    pem_key = f"{pem_header}\n{pem_content}\n{pem_footer}"

    return pem_key

data = {
    "cardNumber": "4111111111111111",
    "expiryMonth": "03",
    "expiryYear": "30",
    "securityCode": "737",
    "holderName": "John Doe",
    "holderReference": "customer123"
}

# '...' should be the public key 
publicKey = format_public_key_to_pem('...')

jsonData = json.dumps(data).encode('utf-8')
# {"cardNumber":"4111111111111111","expiryMonth":"03","expiryYear":"30","securityCode":"737","holderName":"John Doe","holderReference":"customer123"}

encryptedCardData = jwe.encrypt(plaintext=jsonData, algorithm='RSA-OAEP-256', encryption='A256CBC-HS512', key=publicKey)

print(str(encryptedCardData))
# eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIn0...
package com.payrails.android.cse

import com.nimbusds.jose.EncryptionMethod
import com.nimbusds.jose.JWEAlgorithm
import com.nimbusds.jose.JWEHeader
import com.nimbusds.jose.JWEObject
import com.nimbusds.jose.Payload
import com.nimbusds.jose.crypto.RSAEncrypter
import kotlinx.serialization.Serializable
import java.security.KeyFactory
import java.security.interfaces.RSAPublicKey
import java.security.spec.RSAPublicKeySpec
import java.util.Base64

@Serializable
data class InstrumentDetails(
    val holderName: String? = null,
    val cardNumber: String,
    val expiryMonth: String,
    val expiryYear: String,
    val securityCode: String? = null,
    val holderReference: String,
)

fun main() {
    val instrumentDetails = InstrumentDetails(
        holderReference = "customer123",
        holderName = "John Doe",
        cardNumber = "4111111111111111",
        expiryMonth = "12",
        expiryYear = "2025",
        securityCode = "123",
    )

    val instrumentDetailsJson = PayrailsCSE.json.encodeToString(InstrumentDetails.serializer(), instrumentDetails)
    println(instrumentDetailsJson)
    // {"cardNumber":"4111111111111111","expiryMonth":"03","expiryYear":"30","securityCode":"737","holderName":"John Doe","holderReference":"customer123"}

    val header = JWEHeader(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256CBC_HS512)

    val publicKeyStr = "MIIBCgKCAQEAuJeo7zyuzdBx8biauFhVpy9XXk4lgvOe1/xD2G..."
    val publicKey = getPublicKey(publicKeyStr)

    val jwe = JWEObject(header, Payload(instrumentDetailsJson))
    jwe.encrypt(RSAEncrypter(publicKey))

    val encryptedInstrumentDetails = jwe.serialize()
    println(encryptedInstrumentDetails)
    // eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIn0...
}

@Throws(NoSuchAlgorithmException::class, InvalidKeySpecException::class, IOException::class)
private fun getPublicKey(pemPublicKey: String): RSAPublicKey {
  val publicKeyBytes = Base64.getDecoder().decode(pemPublicKey)
  val keySpec = X509EncodedKeySpec(publicKeyBytes)
  val keyFactory = KeyFactory.getInstance("RSA")
  val publicKey = keyFactory.generatePublic(keySpec)
  return publicKey as RSAPublicKey
}

Step 3. Save the card in Payrails

Call the tokenization endpoint with the encrypted payment details.

{
    "id": "bd86c284-44d0-41ad-9f52-3ab1b46f062e",
    "holderReference": "customer123",
    "encryptedInstrumentDetails": "eyJhbGciOiJSU0EtT0F...",
    "futureUsage": "CardOnFile",
    "storeInstrument": true
}

To ensure successful tokenization, please verify that the holder reference provided in the encrypted instrument details matches the holder reference sent in the body request. If the references do not match, the tokenization process will be rejected.