Overview
Use defaultPIN when an endpoint asks you to send a PIN during account setup or a PIN change flow.
You must never send the PIN in plaintext.
Instead, you encode the PIN into a PIN block, encrypt that PIN block with the Cardcore RSA public key, and submit the encrypted result with the matching key fingerprint.
Cardcore decrypts the submitted value on the backend. After decryption, it expects to see a Format 1 PIN block. If the decrypted value is not a valid Format 1 PIN block, the request is rejected.
Object shape
{
"defaultPIN": {
"encryptedData": "A1B2C3D4...",
"publicKeyFingerprint": "A884D774..."
}
}
The RSA-encrypted PIN block.Send this as a hex string.For the current 2048-bit RSA key, the encrypted output is 256 bytes, which becomes a 512-character hex string.Uppercase is recommended for consistency, but the important part is that the value must be valid hex.This value comes from encrypting an 8-byte PIN block with an RSA 2048-bit public key by using PKCS#1 v1.5 padding.This field is non-deterministic.The same PIN will produce a different ciphertext each time because PKCS#1 v1.5 uses random padding.
defaultPIN.publicKeyFingerprint
The fingerprint of the DER-encoded RSA public key used for encryption.The server uses this value to identify the correct private key for decryption.Fetch this fingerprint from the encryption key endpoint before you submit the request.
Why the PIN is encrypted this way
The PIN is treated as sensitive cardholder data.
Cardcore does not accept plaintext PIN values in API requests.
The PIN is first converted into a fixed-length Format 1 PIN block so the server can read it after decryption.
That PIN block is then encrypted with RSA PKCS#1 v1.5 by using the public key published for your environment.
This design gives you these protections:
- The raw PIN never travels over the API as readable text.
- Only the server that holds the matching private key can decrypt the payload.
- Random padding prevents the same PIN from producing the same ciphertext on every request.
- The fingerprint lets the server choose the exact private key that matches the public key you used.
Client integration flow
Follow these steps in your client before you send defaultPIN.
The Cardcore institution dashboard follows this same flow before it creates a card, changes a PIN, or resets a PIN.
1. Fetch the encryption key
Call the encryption key endpoint before you encrypt the PIN.
If you are integrating with Cardcore, use Get RSA key.
Store these values from the response:
data.publicKey
data.publicKeyFingerprint
The publicKey value is a DER public key encoded as a hex string.
The publicKeyFingerprint value must be sent back with the encrypted PIN so the backend knows which key to use.
2. Validate the PIN
Before you encrypt, confirm that the PIN:
- contains only numeric digits
- has a length from
4 to 9 digits
Reject invalid input on the client before you attempt encryption.
The backend currently reads the PIN length as one decimal digit in the Format 1 block. Because of that, use 4 to 9 digit PINs unless your Cardcore environment explicitly confirms support for longer PINs.
The Cardcore institution dashboard currently collects 4-digit PINs.
3. Build the PIN block
Create the PIN block with this format:
"1" + PIN length + PIN digits + hex padding to 16 characters total
The final PIN block must be exactly 16 hex characters. That is 8 bytes after it is converted from hex.
The first character must be 1.
The second character is the PIN length.
The next characters are the PIN digits.
The remaining characters are padding. Use F padding unless your integration has been told to use random hex padding.
Examples:
| PIN | PIN block |
|---|
1234 | 141234FFFFFFFFFF |
123456 | 16123456FFFFFFFF |
123456789 | 19123456789FFFFF |
4. Encrypt the PIN block
Convert the PIN block from hex into bytes before encryption.
Do not encrypt the PIN block as normal text.
For example, 141234FFFFFFFFFF must become the bytes represented by that hex string.
Encrypt those bytes with the RSA public key by using:
- RSA
- PKCS#1 v1.5 padding
- a 2048-bit key
In node-forge, the encryption mode is RSAES-PKCS1-V1_5.
5. Hex-encode the ciphertext
Take the encrypted bytes and hex-encode them in uppercase.
That uppercase hex string becomes defaultPIN.encryptedData.
For a 2048-bit RSA key, the encrypted output is always 256 bytes, which becomes 512 uppercase hex characters.
6. Attach the fingerprint
Copy the fingerprint from the key-fetch response into defaultPIN.publicKeyFingerprint.
Do not derive a different value locally unless your Cardcore integration explicitly tells you to.
Encryption examples
These examples follow the same approach used by the Cardcore institution dashboard.
Browser JavaScript
Node.js
Python
C#
Java
PHP
import forge from 'node-forge';
function encryptPin(pin: string, rsaPublicKeyDerHex: string): string {
if (!/^\d{4,9}$/.test(pin)) {
throw new Error('PIN must be numeric and 4 to 9 digits long.');
}
const pinBlockStart = `1${pin.length}${pin}`;
const padding = 'F'.repeat(16 - pinBlockStart.length);
const pinBlock = pinBlockStart + padding;
const messageBytes = forge.util.hexToBytes(pinBlock);
const derBytes = forge.util.hexToBytes(rsaPublicKeyDerHex);
const asn1 = forge.asn1.fromDer(forge.util.createBuffer(derBytes));
const publicKey = forge.pki.publicKeyFromAsn1(asn1);
const encryptedBytes = publicKey.encrypt(messageBytes, 'RSAES-PKCS1-V1_5');
return forge.util.bytesToHex(encryptedBytes).toUpperCase();
}
import crypto from 'node:crypto';
function encryptPin(pin: string, rsaPublicKeyDerHex: string): string {
if (!/^\d{4,9}$/.test(pin)) {
throw new Error('PIN must be numeric and 4 to 9 digits long.');
}
const pinBlockStart = `1${pin.length}${pin}`;
const padding = 'F'.repeat(16 - pinBlockStart.length);
const pinBlock = pinBlockStart + padding;
const publicKey = crypto.createPublicKey({
key: Buffer.from(rsaPublicKeyDerHex, 'hex'),
format: 'der',
type: 'pkcs1',
});
const encrypted = crypto.publicEncrypt(
{
key: publicKey,
padding: crypto.constants.RSA_PKCS1_PADDING,
},
Buffer.from(pinBlock, 'hex')
);
return encrypted.toString('hex').toUpperCase();
}
from Crypto.Cipher import PKCS1_v1_5
from Crypto.PublicKey import RSA
def encrypt_pin(pin: str, rsa_public_key_der_hex: str) -> str:
if not pin.isdigit() or len(pin) < 4 or len(pin) > 9:
raise ValueError("PIN must be numeric and 4 to 9 digits long.")
pin_block_start = f"1{len(pin)}{pin}"
pin_block = pin_block_start + ("F" * (16 - len(pin_block_start)))
public_key = RSA.import_key(bytes.fromhex(rsa_public_key_der_hex))
cipher = PKCS1_v1_5.new(public_key)
encrypted = cipher.encrypt(bytes.fromhex(pin_block))
return encrypted.hex().upper()
using System;
using System.Security.Cryptography;
public static class PinEncryption
{
public static string EncryptPin(string pin, string rsaPublicKeyDerHex)
{
if (!System.Text.RegularExpressions.Regex.IsMatch(pin, @"^\d{4,9}$"))
{
throw new ArgumentException("PIN must be numeric and 4 to 9 digits long.");
}
var pinBlockStart = $"1{pin.Length}{pin}";
var pinBlock = pinBlockStart + new string('F', 16 - pinBlockStart.Length);
byte[] pinBlockBytes = Convert.FromHexString(pinBlock);
byte[] publicKeyDer = Convert.FromHexString(rsaPublicKeyDerHex);
using RSA rsa = RSA.Create();
try
{
rsa.ImportSubjectPublicKeyInfo(publicKeyDer, out _);
}
catch (CryptographicException)
{
rsa.ImportRSAPublicKey(publicKeyDer, out _);
}
byte[] encrypted = rsa.Encrypt(pinBlockBytes, RSAEncryptionPadding.Pkcs1);
return Convert.ToHexString(encrypted);
}
}
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.RSAPublicKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.math.BigInteger;
import javax.crypto.Cipher;
public class PinEncryption {
public static String encryptPin(String pin, String rsaPublicKeyDerHex) throws Exception {
if (!pin.matches("\\d{4,9}")) {
throw new IllegalArgumentException("PIN must be numeric and 4 to 9 digits long.");
}
String pinBlockStart = "1" + pin.length() + pin;
String pinBlock = pinBlockStart + "F".repeat(16 - pinBlockStart.length());
byte[] pinBlockBytes = hexToBytes(pinBlock);
byte[] publicKeyDer = hexToBytes(rsaPublicKeyDerHex);
PublicKey publicKey = loadPublicKey(publicKeyDer);
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return bytesToHex(cipher.doFinal(pinBlockBytes));
}
private static PublicKey loadPublicKey(byte[] publicKeyDer) throws Exception {
try {
return KeyFactory.getInstance("RSA")
.generatePublic(new X509EncodedKeySpec(publicKeyDer));
} catch (Exception ignored) {
RsaParts parts = readPkcs1PublicKey(publicKeyDer);
return KeyFactory.getInstance("RSA")
.generatePublic(new RSAPublicKeySpec(parts.modulus, parts.exponent));
}
}
private static RsaParts readPkcs1PublicKey(byte[] der) {
DerReader reader = new DerReader(der);
reader.expect(0x30);
reader.readLength();
BigInteger modulus = reader.readInteger();
BigInteger exponent = reader.readInteger();
return new RsaParts(modulus, exponent);
}
private static byte[] hexToBytes(String hex) {
byte[] out = new byte[hex.length() / 2];
for (int i = 0; i < hex.length(); i += 2) {
out[i / 2] = (byte) Integer.parseInt(hex.substring(i, i + 2), 16);
}
return out;
}
private static String bytesToHex(byte[] bytes) {
StringBuilder hex = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
hex.append(String.format("%02X", b));
}
return hex.toString();
}
private static class RsaParts {
final BigInteger modulus;
final BigInteger exponent;
RsaParts(BigInteger modulus, BigInteger exponent) {
this.modulus = modulus;
this.exponent = exponent;
}
}
private static class DerReader {
private final byte[] data;
private int pos = 0;
DerReader(byte[] data) {
this.data = data;
}
void expect(int expectedTag) {
int tag = data[pos++] & 0xff;
if (tag != expectedTag) {
throw new IllegalArgumentException("Invalid DER public key.");
}
}
int readLength() {
int length = data[pos++] & 0xff;
if ((length & 0x80) == 0) {
return length;
}
int count = length & 0x7f;
length = 0;
for (int i = 0; i < count; i++) {
length = (length << 8) | (data[pos++] & 0xff);
}
return length;
}
BigInteger readInteger() {
expect(0x02);
int length = readLength();
byte[] value = java.util.Arrays.copyOfRange(data, pos, pos + length);
pos += length;
return new BigInteger(1, value);
}
}
}
<?php
function derHexToPem(string $derHex): string
{
$base64 = base64_encode(hex2bin($derHex));
return "-----BEGIN RSA PUBLIC KEY-----\n"
. chunk_split($base64, 64, "\n")
. "-----END RSA PUBLIC KEY-----\n";
}
function encryptPin(string $pin, string $rsaPublicKeyDerHex): string
{
if (!preg_match('/^\d{4,9}$/', $pin)) {
throw new InvalidArgumentException('PIN must be numeric and 4 to 9 digits long.');
}
$pinBlockStart = '1' . strlen($pin) . $pin;
$pinBlock = $pinBlockStart . str_repeat('F', 16 - strlen($pinBlockStart));
$publicKeyPem = derHexToPem($rsaPublicKeyDerHex);
$encrypted = '';
$ok = openssl_public_encrypt(
hex2bin($pinBlock),
$encrypted,
$publicKeyPem,
OPENSSL_PKCS1_PADDING
);
if (!$ok) {
throw new RuntimeException('PIN encryption failed.');
}
return strtoupper(bin2hex($encrypted));
}
After encryption, build the request object with the fingerprint from the RSA key response.
const rsa = await getRsaKey();
const encryptedData = encryptPin(pin, rsa.data.publicKey);
const protectedPin = {
encryptedData,
publicKeyFingerprint: rsa.data.publicKeyFingerprint,
};
For a PIN change, encrypt the old PIN and new PIN separately using the same RSA key response.
Use the encrypted PIN in requests
For card creation, send the encrypted PIN as defaultPIN.
{
"defaultPIN": {
"encryptedData": "A1B2C3D4E5F60708A9B0C1D2E3F40516...",
"publicKeyFingerprint": "A884D7749D0E31C1B4E65F8A7B2C1190"
}
}
For PIN change, encrypt the old PIN and new PIN separately.
{
"oldPIN": {
"encryptedData": "A1B2C3D4E5F60708A9B0C1D2E3F40516...",
"publicKeyFingerprint": "A884D7749D0E31C1B4E65F8A7B2C1190"
},
"newPIN": {
"encryptedData": "B1C2D3E4F5061728A9B0C1D2E3F40516...",
"publicKeyFingerprint": "A884D7749D0E31C1B4E65F8A7B2C1190"
}
}
For PIN reset, send only newPIN.
{
"newPIN": {
"encryptedData": "B1C2D3E4F5061728A9B0C1D2E3F40516...",
"publicKeyFingerprint": "A884D7749D0E31C1B4E65F8A7B2C1190"
}
}
Testing note
Do not compare encryptedData by exact value in tests.
RSA PKCS#1 v1.5 encryption is non-deterministic, so the ciphertext changes across runs even when the input PIN is the same.
Instead, validate:
- that the value is uppercase hex
- that the value length is exactly
512
- that the fingerprint matches the key you fetched
- that the server accepts the encrypted payload