Przejdź do głównej zawartości

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.

GET/api/v1_0/cards/public-keyPełny kontrakt endpointu klucza publicznego RSA do szyfrowania danych kart.Pełny kontrakt w API Reference

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

Endpoint zwraca surowy klucz PEM jako tekst (nie JSON):

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----

Klucz jest w formacie PEM (X.509 SubjectPublicKeyInfo). Odczytaj treść odpowiedzi jako string - nie parsuj jej jako JSON.

wskazówka

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

PoleNazwaOpisFormatPrzykład
PNPAN (Primary Account Number)Numer kartyCyfry bez spacji"4111111111111111"
SCSecurity CodeKod CVV/CVC3 cyfry (4 dla Amex)"123"
DTDate (Expiry)Data ważnościMM/YY"12/25"
IDTransaction IDIdentyfikator transakcjiUUID z rejestracji"abc-def-123-456"
TXTimestampCzas operacjiUnix timestamp (sekundy)1700000000
Pole TX

Pole TX służy jako ochrona przed atakami typu replay. dpay.pl odrzuci zapytanie, jeśli timestamp jest starszy niż 600 sekund. Wyjątek: gdy płatność jest wykonywana po potwierdzeniu 3D Secure, akceptowane okno wynosi 3600 sekund. 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 (odpowiedź to surowy PEM jako tekst, nie JSON)
const response = await fetch(
'https://api-payments.dpay.pl/api/v1_0/cards/public-key'
);
const publicKey = await response.text();

// 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 (odpowiedź to surowy PEM jako tekst, nie JSON)
$ch = curl_init('https://api-payments.dpay.pl/api/v1_0/cards/public-key');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$publicKeyPem = curl_exec($ch);
curl_close($ch);

$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 (odpowiedź to surowy PEM jako tekst, nie JSON)
response = requests.get('https://api-payments.dpay.pl/api/v1_0/cards/public-key')
public_key_pem = response.text

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

  1. Szyfrowanie w przeglądarce - surowe dane karty powinny byc szyfrowane po stronie klienta (w JavaScript), zanim trafią na Twoj serwer
  2. Nigdy nie loguj danych kart - nie zapisuj numerow kart, CVV ani dat ważności w logach
  3. HTTPS obowiązkowe - cała strona musi byc dostępna przez HTTPS
  4. Nie przechowuj danych kart - nie zapisuj zaszyfrowanych ani surowych danych kart w bazie danych
  5. Aktualny timestamp - pole TX musi zawierac aktualny czas; dpay.pl odrzuci zapytania z timestampem starszym niż 600 sekund (3600 sekund, gdy płatność następuje po potwierdzeniu 3D Secure)
ważne

Nieprzestrzeganie zasad bezpieczeństwa może skutkowac utrata certyfikacji PCI DSS i odpowiedziałnością prawna za wycieki danych kart płatniczych.