Card Data Encryption (RSA/PKCS#1)
For Server-to-Server (S2S) card payments, card data must be encrypted with an RSA public key before being sent to dpay.pl. This guide describes the entire encryption process in detail.
Why is encryption required?
Raw card data (card number, CVV, expiry date) is sensitive information subject to the PCI DSS standard. RSA encryption ensures that:
- Card data is readable only by dpay.pl (only dpay.pl holds the private key)
- Even if data is intercepted in transit, it will be useless without the private key
- Your server never has access to raw card data (encryption takes place in the customer's browser)
Encryption algorithm
- Algorithm: RSA
- Padding: PKCS#1 v1.5
- Output encoding: Base64
Step 1: Retrieve the public key
GET https://api-payments.dpay.pl/api/v1_0/cards/public-key
Response
{
"publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...\n-----END PUBLIC KEY-----"
}
The key is in PEM format (X.509 SubjectPublicKeyInfo).
The public key may be rotated without notice. Fetch a new public key before each transaction.
Step 2: Prepare the JSON object
Prepare a JSON object containing the card data and transaction metadata:
{
"PN": "4111111111111111",
"SC": "123",
"DT": "12/25",
"ID": "abc-def-123-456",
"TX": 1700000000
}
Field descriptions
| Field | Name | Description | Format | Example |
|---|---|---|---|---|
PN | PAN (Primary Account Number) | Card number | Digits without spaces | "4111111111111111" |
SC | Security Code | CVV/CVC code | 3 digits (4 for Amex) | "123" |
DT | Date (Expiry) | Expiry date | MM/YY | "12/25" |
ID | Transaction ID | Transaction identifier | UUID from registration | "abc-def-123-456" |
TX | Timestamp | Operation time | Unix timestamp (seconds) | 1700000000 |
The TX field serves as protection against replay attacks. dpay.pl will reject requests if the timestamp is older than a few minutes. Always use the current time.
Step 3: Encryption
TypeScript / JavaScript (JSEncrypt)
The JSEncrypt library is the most popular choice for RSA encryption in the browser:
npm install jsencrypt
import JSEncrypt from 'jsencrypt';
interface CardData {
PN: string; // Card number
SC: string; // CVV
DT: string; // Expiry date MM/YY
ID: string; // Transaction ID
TX: number; // Unix timestamp
}
async function encryptCardData(
cardNumber: string,
cvv: string,
expiryDate: string,
transactionId: string
): Promise<string> {
// 1. Fetch the public key
const response = await fetch(
'https://api-payments.dpay.pl/api/v1_0/cards/public-key'
);
const { publicKey } = await response.json();
// 2. Prepare the card data object
const cardData: CardData = {
PN: cardNumber.replace(/\s/g, ''), // Remove spaces
SC: cvv,
DT: expiryDate, // Format: MM/YY
ID: transactionId,
TX: Math.floor(Date.now() / 1000),
};
// 3. Encrypt with RSA/PKCS#1
const encrypt = new JSEncrypt();
encrypt.setPublicKey(publicKey);
const encrypted = encrypt.encrypt(JSON.stringify(cardData));
if (!encrypted) {
throw new Error('Card data encryption failed');
}
// 4. The result is already Base64-encoded
return encrypted;
}
// === Usage example ===
const encryptedData = await encryptCardData(
'4111 1111 1111 1111',
'123',
'12/25',
'abc-def-123-456'
);
// Send encryptedData to your server
await fetch('/api/pay/card', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ encryptedCardData: encryptedData }),
});
PHP (OpenSSL)
Encryption in PHP using the built-in OpenSSL extension:
<?php
/**
* Card data encryption RSA/PKCS#1
*
* NOTE: In production, encryption should take place on the client side
* (in the browser) so that raw card data never reaches your server.
* This example is for demonstration and testing purposes only.
*/
function encryptCardData(
string $cardNumber,
string $cvv,
string $expiryDate,
string $transactionId,
string $publicKeyPem
): string {
// 1. Prepare the JSON object
$cardData = json_encode([
'PN' => preg_replace('/\s/', '', $cardNumber),
'SC' => $cvv,
'DT' => $expiryDate,
'ID' => $transactionId,
'TX' => time(),
], JSON_UNESCAPED_SLASHES);
// 2. Load the public key
$publicKey = openssl_pkey_get_public($publicKeyPem);
if (!$publicKey) {
throw new RuntimeException(
'Failed to load public key: ' . openssl_error_string()
);
}
// 3. Encrypt with RSA/PKCS#1
$encrypted = '';
$result = openssl_public_encrypt(
$cardData,
$encrypted,
$publicKey,
OPENSSL_PKCS1_PADDING
);
if (!$result) {
throw new RuntimeException(
'Encryption failed: ' . openssl_error_string()
);
}
// 4. Encode in Base64
return base64_encode($encrypted);
}
// === Usage example ===
// Fetch the public key
$ch = curl_init('https://api-payments.dpay.pl/api/v1_0/cards/public-key');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
$publicKeyPem = $response['publicKey'];
$encryptedCardData = encryptCardData(
'4111111111111111',
'123',
'12/25',
'abc-def-123-456',
$publicKeyPem
);
echo $encryptedCardData; // Base64-encoded encrypted data
Python (pycryptodome)
pip install pycryptodome
import json
import time
import base64
import re
import requests
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
def encrypt_card_data(
card_number: str,
cvv: str,
expiry_date: str,
transaction_id: str,
public_key_pem: str,
) -> str:
"""Encrypt card data with RSA/PKCS#1."""
# 1. Prepare the JSON object
card_data = json.dumps({
'PN': re.sub(r'\s', '', card_number),
'SC': cvv,
'DT': expiry_date,
'ID': transaction_id,
'TX': int(time.time()),
})
# 2. Load the public key
key = RSA.import_key(public_key_pem)
# 3. Encrypt with RSA/PKCS#1 v1.5
cipher = PKCS1_v1_5.new(key)
encrypted = cipher.encrypt(card_data.encode('utf-8'))
# 4. Encode in Base64
return base64.b64encode(encrypted).decode('utf-8')
# === Usage example ===
# Fetch the public key
response = requests.get('https://api-payments.dpay.pl/api/v1_0/cards/public-key')
public_key_pem = response.json()['publicKey']
encrypted = encrypt_card_data(
'4111111111111111',
'123',
'12/25',
'abc-def-123-456',
public_key_pem,
)
print(encrypted)
Java
import javax.crypto.Cipher;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public class CardEncryption {
public static String encryptCardData(
String cardNumber,
String cvv,
String expiryDate,
String transactionId,
String publicKeyPem
) throws Exception {
// 1. Prepare the JSON
String cardData = String.format(
"{\"PN\":\"%s\",\"SC\":\"%s\",\"DT\":\"%s\",\"ID\":\"%s\",\"TX\":%d}",
cardNumber.replaceAll("\\s", ""),
cvv,
expiryDate,
transactionId,
System.currentTimeMillis() / 1000
);
// 2. Load the public key
String keyContent = publicKeyPem
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s", "");
byte[] keyBytes = Base64.getDecoder().decode(keyContent);
X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
PublicKey publicKey = KeyFactory.getInstance("RSA").generatePublic(spec);
// 3. Encrypt with RSA/PKCS#1
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] encrypted = cipher.doFinal(cardData.getBytes("UTF-8"));
// 4. Encode in Base64
return Base64.getEncoder().encodeToString(encrypted);
}
}
Card data validation before encryption
It is good practice to validate card data on the client side before encryption:
Luhn algorithm (card number validation)
function isValidCardNumber(number) {
const digits = number.replace(/\s/g, '');
if (!/^\d{13,19}$/.test(digits)) return false;
let sum = 0;
let isEven = false;
for (let i = digits.length - 1; i >= 0; i--) {
let digit = parseInt(digits[i], 10);
if (isEven) {
digit *= 2;
if (digit > 9) digit -= 9;
}
sum += digit;
isEven = !isEven;
}
return sum % 10 === 0;
}
Expiry date validation
function isValidExpiryDate(date) {
const match = date.match(/^(\d{2})\/(\d{2})$/);
if (!match) return false;
const month = parseInt(match[1], 10);
const year = parseInt('20' + match[2], 10);
if (month < 1 || month > 12) return false;
const now = new Date();
const expiry = new Date(year, month); // First day of the following month
return expiry > now;
}
CVV validation
function isValidCVV(cvv) {
return /^\d{3,4}$/.test(cvv);
}
Security - best practices
- Encrypt in the browser - raw card data should be encrypted on the client side (in JavaScript) before reaching your server
- Never log card data - do not store card numbers, CVVs, or expiry dates in logs
- HTTPS mandatory - your entire site must be served over HTTPS
- Do not store card data - do not save encrypted or raw card data in your database
- Current timestamp - the
TXfield must contain the current time; dpay.pl will reject requests with an outdated timestamp
Failure to comply with security requirements may result in loss of PCI DSS certification and legal liability for payment card data breaches.