Szyfrowanie danych kart (RSA/PKCS#1)
W przypadku płatności kartowych Server-to-Server (S2S) dane karty muszą byc zaszyfrowane kluczem publicznym RSA przed wysłaniem do dpay.pl. Ten przewodnik opisuje szczegółowo cały proces szyfrowania.
Dlaczego szyfrowanie jest wymagane?
Surowe dane karty (numer, CVV, data ważności) to dane wrażliwe objęte standardem PCI DSS. Szyfrowanie RSA zapewnia, ze:
- Dane karty sa czytelne wyłącznie dla dpay.pl (tylko dpay.pl posiada klucz prywatny)
- Nawet jeśli dane zostana przejete w tranzycie, będą bezużyteczne bez klucza prywatnego
- Twoj serwer nigdy nie ma dostępu do surowych danych karty (szyfrowanie odbywa się w przeglądarce klienta)
Algorytm szyfrowania
- Algorytm: RSA
- Padding: PKCS#1 v1.5
- Kodowanie wyniku: Base64
Krok 1: Pobranie klucza publicznego
GET https://api-payments.dpay.pl/api/v1_0/cards/public-key
Odpowiedz
{
"publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...\n-----END PUBLIC KEY-----"
}
Klucz jest w formacie PEM (X.509 SubjectPublicKeyInfo).
Klucz publiczny może być rotowany bez zapowiedzi. Przed każdą transakcją, pobierz nowy klucz publiczny.
Krok 2: Przygotowanie obiektu JSON
Przygotuj obiekt JSON z danymi karty i metadanymi transakcji:
{
"PN": "4111111111111111",
"SC": "123",
"DT": "12/25",
"ID": "abc-def-123-456",
"TX": 1700000000
}
Opis pol
| Pole | Nazwa | Opis | Format | Przykład |
|---|---|---|---|---|
PN | PAN (Primary Account Number) | Numer karty | Cyfry bez spacji | "4111111111111111" |
SC | Security Code | Kod CVV/CVC | 3 cyfry (4 dla Amex) | "123" |
DT | Date (Expiry) | Data ważności | MM/YY | "12/25" |
ID | Transaction ID | Identyfikator transakcji | UUID z rejestracji | "abc-def-123-456" |
TX | Timestamp | Czas operacji | Unix timestamp (sekundy) | 1700000000 |
Pole TX służy jako ochrona przed atakami typu replay. dpay.pl odrzuci zapytanie, jeśli timestamp jest starszy niz kilka minut. Zawsze użyj aktualnego czasu.
Krok 3: Szyfrowanie
TypeScript / JavaScript (JSEncrypt)
Biblioteka JSEncrypt to najpopularniejszy wybór do szyfrowania RSA w przeglądarce:
npm install jsencrypt
import JSEncrypt from 'jsencrypt';
interface CardData {
PN: string; // Numer karty
SC: string; // CVV
DT: string; // Data ważności MM/YY
ID: string; // Transaction ID
TX: number; // Unix timestamp
}
async function encryptCardData(
cardNumber: string,
cvv: string,
expiryDate: string,
transactionId: string
): Promise<string> {
// 1. Pobierz klucz publiczny
const response = await fetch(
'https://api-payments.dpay.pl/api/v1_0/cards/public-key'
);
const { publicKey } = await response.json();
// 2. Przygotuj obiekt z danymi karty
const cardData: CardData = {
PN: cardNumber.replace(/\s/g, ''), // Usun spacje
SC: cvv,
DT: expiryDate, // Format: MM/YY
ID: transactionId,
TX: Math.floor(Date.now() / 1000),
};
// 3. Zaszyfruj RSA/PKCS#1
const encrypt = new JSEncrypt();
encrypt.setPublicKey(publicKey);
const encrypted = encrypt.encrypt(JSON.stringify(cardData));
if (!encrypted) {
throw new Error('Szyfrowanie danych karty nie powiodło sie');
}
// 4. Wynik jest juz w Base64
return encrypted;
}
// === Przykład użycia ===
const encryptedData = await encryptCardData(
'4111 1111 1111 1111',
'123',
'12/25',
'abc-def-123-456'
);
// Wyślij encryptedData do serwera
await fetch('/api/pay/card', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ encryptedCardData: encryptedData }),
});
PHP (OpenSSL)
Szyfrowanie w PHP za pomoca wbudowanego rozszerzenia OpenSSL:
<?php
/**
* Szyfrowanie danych karty RSA/PKCS#1
*
* UWAGA: W produkcji szyfrowanie powinno odbywac się po stronie klienta
* (w przeglądarce), aby surowe dane karty nigdy nie trafiły na Twoj serwer.
* Ten przykład służy do celów demonstracyjnych i testowych.
*/
function encryptCardData(
string $cardNumber,
string $cvv,
string $expiryDate,
string $transactionId,
string $publicKeyPem
): string {
// 1. Przygotuj obiekt JSON
$cardData = json_encode([
'PN' => preg_replace('/\s/', '', $cardNumber),
'SC' => $cvv,
'DT' => $expiryDate,
'ID' => $transactionId,
'TX' => time(),
], JSON_UNESCAPED_SLASHES);
// 2. Zaladuj klucz publiczny
$publicKey = openssl_pkey_get_public($publicKeyPem);
if (!$publicKey) {
throw new RuntimeException(
'Nie można zaladowac klucza publicznego: ' . openssl_error_string()
);
}
// 3. Zaszyfruj RSA/PKCS#1
$encrypted = '';
$result = openssl_public_encrypt(
$cardData,
$encrypted,
$publicKey,
OPENSSL_PKCS1_PADDING
);
if (!$result) {
throw new RuntimeException(
'Szyfrowanie nie powiodło sie: ' . openssl_error_string()
);
}
// 4. Zakoduj w Base64
return base64_encode($encrypted);
}
// === Przykład użycia ===
// Pobierz klucz publiczny
$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:
"""Szyfrowanie danych karty RSA/PKCS#1."""
# 1. Przygotuj obiekt JSON
card_data = json.dumps({
'PN': re.sub(r'\s', '', card_number),
'SC': cvv,
'DT': expiry_date,
'ID': transaction_id,
'TX': int(time.time()),
})
# 2. Zaladuj klucz publiczny
key = RSA.import_key(public_key_pem)
# 3. Zaszyfruj RSA/PKCS#1 v1.5
cipher = PKCS1_v1_5.new(key)
encrypted = cipher.encrypt(card_data.encode('utf-8'))
# 4. Zakoduj w Base64
return base64.b64encode(encrypted).decode('utf-8')
# === Przykład użycia ===
# Pobierz klucz publiczny
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. Przygotuj 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. Zaladuj klucz publiczny
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. Zaszyfruj 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. Zakoduj w Base64
return Base64.getEncoder().encodeToString(encrypted);
}
}
Walidacja danych karty przed szyfrowaniem
Przed szyfrowaniem warto zwalidowac dane karty po stronie klienta:
Algorytm Luhna (walidacja numeru karty)
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;
}
Walidacja daty ważności
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); // Pierwszy dzien następnego miesiaca
return expiry > now;
}
Walidacja CVV
function isValidCVV(cvv) {
return /^\d{3,4}$/.test(cvv);
}
Bezpieczeństwo - najlepsze praktyki
- Szyfrowanie w przeglądarce - surowe dane karty powinny byc szyfrowane po stronie klienta (w JavaScript), zanim trafią na Twoj serwer
- Nigdy nie loguj danych kart - nie zapisuj numerow kart, CVV ani dat ważności w logach
- HTTPS obowiązkowe - cała strona musi byc dostępna przez HTTPS
- Nie przechowuj danych kart - nie zapisuj zaszyfrowanych ani surowych danych kart w bazie danych
- Aktualny timestamp - pole
TXmusi zawierac aktualny czas; dpay.pl odrzući zapytania ze starym timestampem
Nieprzestrzeganie zasad bezpieczeństwa może skutkowac utrata certyfikacji PCI DSS i odpowiedziałnością prawna za wycieki danych kart płatniczych.