Appearance
Displaying sensitive data
PCI DSS allows for an enhanced experience for the cardholders. You can display sensitive information as an image or a text, and instantly copy it. This applies to the card PIN, number (PAN), and verification code (CVV).
You must use asymmetrical end-to-end encryption when it comes to exposing the following card data.
Sensitive data | Endpoint |
---|---|
Card Image | /cards/{cardId}/cardImage |
PAN & CVV | /cards/{cardId}/pan |
PIN | /cards/{cardId}/pin |
API – Swagger documentation available
For a complete list of attributes for these endpoints, check the PCI DSS Swagger.
This article focuses on the /cards/{cardId}/pan
for the examples, but the same process applies for all endpoints containing sensitive data.
Process
Here are the steps for exposing sensitive data.
1. Generate an asymmetrical key pair on the end user’s device
Asymmetrical encryption consists of using a key pair, with each key fulfilling its specific role:
- Public key – Used to encrypt data
- Private key – Used to decrypt data, and can’t be shared sensitive data
Security – Generate at least 1 key pair by user
Key pairs must be unique for each of your end users. For optimal security, you should rely on single-use key pairs (i.e., generate a key pair for each request).
Treezor PCI DSS-specific endpoints support 2 encryption methods: OPENPGP_ARMOR
or LIBSODIUM_HEX
.
Example for OpenPGP
Best practice – Formatting your keys with OpenPGP
- Type: RSA
- Size: min. 2048, recommended 4096
Find below an extract of the functional code available at the end of the article.
js
/* import openpgp v5.11.2. You can find it here https://github.com/openpgpjs/openpgpjs/blob/main/README.md#getting-started */
function PgpStrategy() {
this.privateKey = ''
this.publicKey = ''
this.passphrase = ''
this.option = {
userIDs: [{ name: "your_company_name", email: "your_company_email@email.com" }], // TODO replace name and email by yours.
type:'rsa',
rsaBits: "4096",
passphrase: this.passphrase,
}
...
return {
generateKeyPair: async () => {
const keyPair = await openpgp.generateKey(this.option)
this.privateKey = keyPair.privateKey
this.publicKey =keyPair.publicKey
return this.publicKey
},
...
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Example for Libsodium
Find below an extract of the functional code available at the end of the article.
js
/* import libsodium-wrappers v0.5.4. You can find it here https://www.npmjs.com/package/libsodium-wrappers?activeTab=readme */
function LibsodiumStrategy () {
this.privateKey = ''
this.publicKey = ''
return {
...
generateKeyPair: async () => {
await sodium.ready
const keyPair = await sodium.crypto_box_keypair()
this.privateKey = sodium.to_hex(keyPair.privateKey)
this.publicKey = sodium.to_hex(keyPair.publicKey)
return this.publicKey
},
...
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Now that the key pair has been generated on the end user's device, you can send the public key on your servers to then pass it along to Treezor in the next step.
However, it is strictly forbidden to share the private key, only the cardholder's device can access it.
2. Make your Treezor request with the cardholder's public key
Make your Treezor request with the cardholder's public key from your server using the mTLS and signature mechanisms described in the PCI DSS integration article.
The following parameters must be used for Treezor sensitive endpoints:
algorithm
– The encryption method used (OPENPGP_ARMOR
orLIBSODIUM_HEX
)userPublicKeyBase64
– The previously generated encrypted user public key (base64-encoded).
Here is an example when retrieving the card PAN and CVV (/cards/{cardId}/pan
).
go
package main
import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
)
type getEncryptedCardDataResponse struct {
EncryptedCardData string `json:"encryptedCardData,omitempty"`
}
// GetPAN sends a PCI request to retrieve the PAN.
func GetPAN() {
request, err := GenerateRequest(
http.MethodGet,
"https://client<your_company_name>.preprod.secure.treezor.co/cards/1234/pan",
map[string]string{
"algorithm": "OPENPGP_ARMOR",
"userId": "5678",
"withCVV": "true",
"userPublicKey": base64.StdEncoding.EncodeToString([]byte("cardholder's OpenPGP public key value")),
"sca": "sca-proof-value",
},
"jwtToken",
"scaProof",
nil,
)
if err != nil {
log.Fatalf("failed to generate request: %s", err)
}
response, err := SendUsingMTLS(request)
if err != nil {
log.Fatalf("failed to send request: %s", err)
}
if response.StatusCode != http.StatusOK {
body, _ := io.ReadAll(response.Body)
log.Fatalf("unexpected status code: %d with body: %s", response.StatusCode, string(body))
}
getCardDataResponse := &getEncryptedCardDataResponse{}
err = json.NewDecoder(response.Body).Decode(&getCardDataResponse)
if err != nil {
log.Fatalf("failed to read get encrypted card data response: %s", err)
}
// Base64 encrypted content
fmt.Println(getCardDataResponse.EncryptedCardData)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
php
<?php
use GuzzleHttp\Client;
use Monolog\Logger;
use Psr\Http\Client\ClientExceptionInterface;
const PGP_FILE = 'pgp.pub';
/**
* GetPAN sends a PCI request to retrieve the PAN
*/
function getPAN(string $jwtToken, string $scaProof): void
{
$logger = new Logger('fake-logger');
$queryParams = [
'algorithm' => 'OPENPGP_ARMOR',
'userId' => '8364813',
'withCVV' => true,
'userPublicKey' => base64_encode(file_get_contents(PGP_FILE)),
'sca' => $scaProof,
];
try {
$request = generateRequest(
'GET',
'https://<your_company_name>.preprod.secure.treezor.co/cards/1234/pan',
$queryParams,
$jwtToken,
$scaProof,
''
);
} catch (RuntimeException $exception) {
$logger->error(sprintf('Failed to generate request %s', $exception->getMessage()));
return;
}
try {
$response = sendUsingMTLS($request);
} catch (ClientExceptionInterface $exception) {
$logger->error(sprintf('Failed to send request %s', $exception->getMessage()));
return;
}
if ($response->getStatusCode() !== 200) {
throw new RuntimeException(sprintf('Unexpected status code: %d with body: %s', $response->getStatusCode(), $response->getBody()->getContents()));
}
$cardDataResponse = json_decode($response->getBody()->getContents(), true);
if (!$cardDataResponse) {
$logger->error('Failed to read get encrypted card data response');
}
// Base64 encrypted content
echo $cardDataResponse;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
The Treezor returns the base64-encrypted data.
For example, the response for the /cards/{cardId}/pan
request.
json
{
"encryptedCardData": "<BASE64 ENCRYPTED CONTENT>"
}
1
2
3
2
3
The encrypted data must be sent to the end user's device for the decryption step.
3. Use the private key on the end users' device for decryption
Once the cardholder's device retrieved the encrypted data, it can use the private key for decryption.
Example for OpenPGP
Find below an extract of the functional code available at the end of the article.
js
/* import openpgp v5.11.2. You can find it here https://github.com/openpgpjs/openpgpjs/blob/main/README.md#getting-started */
function PgpStrategy() {
this.privateKey = ''
this.publicKey = ''
this.passphrase = ''
return {
decrypt: async (cypherText) => {
const privKey = await openpgp.decryptKey({
privateKey: await openpgp.readPrivateKey({ armoredKey: this.privateKey }),
passphrase: this.passphrase
});
return await openpgp.decrypt({
message: await openpgp.readMessage({armoredMessage:cypherText}),
decryptionKeys: privKey
});
},
...
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Example for Libsodium
Find below an extract of the functional code available at the end of the article.
js
/* import libsodium-wrappers v0.5.4. You can find it here https://www.npmjs.com/package/libsodium-wrappers?activeTab=readme */
function LibsodiumStrategy () {
this.privateKey = ''
this.publicKey = ''
return {
decrypt: async (hexCypherText) => {
let outputFormat = 'text';
return await sodium.crypto_box_seal_open(
sodium.from_hex(hexCypherText),
sodium.from_hex(this.publicKey),
sodium.from_hex(this.privateKey),
outputFormat
)
},
...
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Decrypted data example
Once decrypted, here is the payload you should have for the /cards/{cardId}/pan
request, whether you chose to include the CVV or not.
json
{
"pan": "1234567898765432",
"cvv": "123"
}
1
2
3
4
2
3
4
json
{
"pan": "1234567898765432"
}
1
2
3
2
3
Security – Destroy sensitive data once viewed by the cardholder
You must destroy both the decrypted data and the asymmetrical key pair after usage. If the user wishes to view their card information again, you must redo the whole process.
Full examples
Example for OpenPGP
Best practice – Formatting your keys with OpenPGP
- Type: RSA
- Size: min. 2048, recommended 4096
js
/* import openpgp v5.11.2. You can find it here https://github.com/openpgpjs/openpgpjs/blob/main/README.md#getting-started */
function PgpStrategy() {
this.algoName = 'OPENPGP_ARMOR'
this.privateKey = ''
this.publicKey = ''
const generateRandomPassphrase = length => {
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+{}[]|;:<>,.?/~";
let passphrase = "";
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * charset.length);
passphrase += charset[randomIndex];
}
return passphrase;
}
this.passphrase = generateRandomPassphrase(16)
this.option = {
userIDs: [{ name: "cardholder", email: "cardholder_email@example.com" }], // TODO replace name and email by yours.
type:'rsa',
rsaBits: "4096",
passphrase: this.passphrase,
}
return {
getAlgoName: () => {
return this.algoName
},
generateKeyPair: async () => {
const keyPair = await openpgp.generateKey(this.option)
this.privateKey = keyPair.privateKey
this.publicKey =keyPair.publicKey
return this.publicKey
},
// TODO remove encrypt function. needed only to simulate PCI-DSS encryption in this code snippet.
encrypt: async (textToEncrypt) => {
const pubKey = await openpgp.readKey({ armoredKey: this.publicKey})
return btoa(await openpgp.encrypt({
message: await openpgp.createMessage({ text: textToEncrypt}),
encryptionKeys: pubKey
}))
},
decrypt: async (cypherText) => {
const privKey = await openpgp.decryptKey({
privateKey: await openpgp.readPrivateKey({ armoredKey: this.privateKey }),
passphrase: this.passphrase
});
return await openpgp.decrypt({
message: await openpgp.readMessage({armoredMessage:cypherText}),
decryptionKeys: privKey
});
},
removeKeyPair: () => {
this.privateKey = ''
this.publicKey = ''
this.passphrase = ''
}
}
};
// displaySensitiveData function on cardholder application.
async function displaySensitiveData(){
let pgp= new PgpStrategy()
pgp.generateKeyPair().then((pubKey)=>{
// TODO replace the pgp.encrypt call below by the call to your backend with needed data, for example:
// call(cardId, userId, pgp.getAlgoName(), pubKey,...).then(base64EncodedEncryptedData => {...
pgp.encrypt('1234').then(base64EncodedEncryptedData => {
// keep code below
pgp.decrypt(atob(base64EncodedEncryptedData))
.then(decrypted =>{console.log('decrypted:'+decrypted.data)})
.catch(err=> {console.log(err)})
.finally(function(){
pgp.removeKeyPair();
});
}).catch(err => {console.log(err)});
}).catch(err=> {console.log(err)})
}
displaySensitiveData()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
Example for Libsodium
js
/* import libsodium-wrappers v0.5.4. You can find it here https://www.npmjs.com/package/libsodium-wrappers?activeTab=readme */
function LibsodiumStrategy () {
this.algoName = 'LIBSODIUM_HEX'
this.privateKey = ''
this.publicKey = ''
return {
getAlgoName: () => {
return this.algoName
},
generateKeyPair: async () => {
await sodium.ready
const keyPair = await sodium.crypto_box_keypair()
this.privateKey = sodium.to_hex(keyPair.privateKey)
this.publicKey = sodium.to_hex(keyPair.publicKey)
return this.publicKey
},
decrypt: async (hexCypherText) => {
let outputFormat = 'text';
return await sodium.crypto_box_seal_open(
sodium.from_hex(hexCypherText),
sodium.from_hex(this.publicKey),
sodium.from_hex(this.privateKey),
outputFormat
)
},
// TODO remove encrypt function. needed only to simulate PCI-DSS encryption in this code snippet.
encrypt: (plainText) => {
const cypherText = sodium.crypto_box_seal(
plainText,
sodium.from_hex(this.publicKey)
)
return btoa(sodium.to_hex(cypherText));
},
removeKeyPair: () => {
this.privateKey = ''
this.publicKey = ''
}
}
}
// displaySensitiveData function on cardholder application.
async function displaySensitiveData(){
const libso= new LibsodiumStrategy()
libso.generateKeyPair().then((pubKey) =>{
// TODO replace the libso.encrypt call below by the call to your backend with needed data, for example:
// call(cardId, userId, libso.getAlgoName(), pubKey,...).then(base64EncodedEncryptedData => {...
const base64EncodedEncryptedData = libso.encrypt('1234')
// keep code below
libso.decrypt(atob(base64EncodedEncryptedData))
.then(decrypted =>{console.log('decrypted:'+decrypted)})
.catch(err=> {console.log(err)})
.finally(function(){
libso.removeKeyPair();
});
}).catch(err=> {console.log(err)})
}
displaySensitiveData()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65