Client-Side Encryption
- Introduction
- Basic workflow
- Authentication
- CSE webservices
- Encrypting form data
- Sending a payment request
- Requirements
Introduction
Client-Side Encryption is an integration mode aimed at limiting the transition of sensitive PAN data only between the user’s browser and the Dalenys payment web-service. It reduces the PCI-DSS liability level of the merchants concerned by a “directlink” server-to-server payment integration, from a SAQ D to a SAQ AEP.
Here is an example of simple integration of a client-server payment in CSE mode:
tips
You can see this live example at: https://codepen.io/dalenys-paymentbe2bill/pen/oNBoxMP
See the Pen Basic Dalenys-Payment ONEY CSE by Dalenys (@dalenys-payment) on CodePen.
Basic workflow
Here is a simple diagram illustrating the CSE workflow. The workflow is similar to the Hosted-fields mode, except that you manage your own form. So you have to implement some JavaScript to encrypt the sensitive data before sending them.
- You display a payment page including a payment form, implementing the encryption JavaScript code.
- At the submit process, you should request an encryption public-key (RSA-OAEP) from
cseKeys
provider service. - Dalenys returns you the public-key.
- You encrypt the form values and send it to the
tokenizer
service. - Dalenys returns you a token.
- Then you must add the received token (instead of the card data) to your HTTPS POST request to our classical server to server endpoint: https://secure-test.dalenys.com/front/service/rest/process.
- The Dalenys platform sends a request to the bank network and waits for the result.
- You receive the result in the request response.
- In parallel, the transaction result is confirmed by a notification request sent to the merchant’s
NOTIFICATION_URL
containing the transaction’s parameters (among whichEXECCODE
andTRANSACTIONID
).
info
The
NOTIFICATION_URL
can be configured through the Dalenys Dashboard, in the technical account configuration.
Authentication
The cseKeys
and tokenizer
services use the API Keys authentication mode
with a Public
key type.
CSE webservices
cseKeys : Encryption keys provider
This service provides the public RSA key needed to encrypt the clients card data.
Endpoint : https://sandbox-payment.dlns.io/cseKeys
Example POST Request (application/json) :
{
"apiKeyId": "fadc44f6-b98b-4ea1-a8a0-50ab1d2e216f"
}
Example Response (application/json) :
{
"encryptionKeyId": "2bbcb1d1-6b26-47b0-aa5e-32f93541cb55",
"encryptionPublicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCA..."
}
tokenizer : PAN data tokenization service
This service decrypts the PAN data and provides a uniq token associated with the client card.
Endpoint : https://secure-test.dalenys.com/hosted-fields/service/tokenize
Example POST Request (application/json) :
{
"ENCRYPTEDDATA": "OSy/asnvr9EQaM6ULFU5aDAZ6cJzHSEEo...==",
"ENCRYPTIONKEYID": "2bbcb1d1-6b26-47b0-aa5e-32f93541cb55",
"APIKEYID": "fadc44f6-b98b-4ea1-a8a0-50ab1d2e216f"
}
Example Response (application/json) :
{
"EXECCODE": "0000",
"MESSAGE": "Operation succeeded.",
"HFTOKEN": "a22faa79-b237-4f21-bf5c-60f83dca3a22",
"CARDTYPE": "VISA",
"CARDVALIDITYDATE": "12-23",
"CARDCODE": "411111XXXXXX1111",
"SELECTEDBRAND": "VISA",
"BINTYPE": "unknown",
"BINBRANDS": [
{
"BRAND": "VISA",
"SERVICETYPE": null
}
],
"BINNETWORK": "VISA"
}
Encrypting form data
Step 1: Implement CSE JavaScript lib
First, you must implement the CSE JavaScript. This is just a tested and working example of the code needed to encrypt the data with an RSA-OAEP public key. Feel free to adapt the code or rewrite it to match your needs.
// Encryption and submition script
(function (window) {
function createXhrPost(serviceUrl, resolve) {
let xhr = new XMLHttpRequest();
xhr.open("POST", serviceUrl, true);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
let json = JSON.parse(xhr.responseText);
resolve(json);
}
};
return xhr;
}
function fetchPublicKey(serviceUrl, apiKeyId) {
return new Promise((resolve) => {
let xhr = createXhrPost(serviceUrl, resolve);
let data = JSON.stringify({
apiKeyId: apiKeyId,
});
xhr.send(data);
});
}
function encryptPlainText(plainText, publicKey) {
let cryptoSubtle;
let btoa = window.btoa;
let atob = window.atob;
// Fix Apple prefix if needed
if (
window.crypto &&
!window.crypto.subtle &&
window.crypto.webkitSubtle
) {
cryptoSubtle = window.crypto.subtle;
} else if (window.crypto && window.crypto.subtle) {
cryptoSubtle = window.crypto.subtle;
}
if (!cryptoSubtle) {
throw "No crypto api";
}
function getMessageEncoding(message) {
let enc = new TextEncoder();
return enc.encode(message);
}
function encryptMessage(publicKey, message) {
let encoded = getMessageEncoding(message);
return cryptoSubtle.encrypt(
{
name: "RSA-OAEP",
},
publicKey,
encoded
);
}
function arrayBufferToBase64(buffer) {
let binary = "";
let bytes = new Uint8Array(buffer);
let len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
function strToArrayBuffer(str) {
const buf = new ArrayBuffer(str.length);
let bufView = new Uint8Array(buf);
for (let i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
}
function importRsaKey(pem) {
// base64 decode the string to get the binary data
const binaryDerString = atob(pem);
// convert from a binary string to an ArrayBuffer
const binaryDer = strToArrayBuffer(binaryDerString);
return cryptoSubtle.importKey(
"spki",
binaryDer,
{
name: "RSA-OAEP",
hash: "SHA-1",
},
true,
["encrypt"]
);
}
return importRsaKey(publicKey)
.then((binaryPublicKey) => {
return encryptMessage(binaryPublicKey, plainText);
})
.then((encryptedBytes) => {
return arrayBufferToBase64(encryptedBytes);
});
}
/**
* serviceUrl: form post url
* postPayload: expected content {
* "ENCRYPTEDDATA": base64Message,
* "ENCRYPTIONKEYID": encryptionKeyId,
* "APIKEYID": apiKeyId
* };
* */
function postEncryptedData(serviceUrl, postPayload) {
return fetch(serviceUrl, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(postPayload),
})
.then(async function (response) {
return await response.json();
})
.catch((error) => {
return { error: error };
});
}
async function preparePayload(cseServiceUrl, apiKeyId, cardInfo) {
let plainText = JSON.stringify(cardInfo);
let rsaPublicKey = await cseLib.fetchPublicKey(cseServiceUrl, apiKeyId);
let encryptedMsd = await cseLib.encryptPlainText(
plainText,
rsaPublicKey.encryptionPublicKey
);
return {
ENCRYPTEDDATA: encryptedMsd,
ENCRYPTIONKEYID: rsaPublicKey.encryptionKeyId,
APIKEYID: apiKeyId,
};
}
window.cseLib = {
fetchPublicKey: fetchPublicKey,
encryptPlainText: encryptPlainText,
preparePayload: preparePayload,
postEncryptedData: postEncryptedData,
};
})(window);
security
You must own a TLS certificate to host a valid HTTPS payment page, otherwise the user’s browser will display security alerts and is likely to block it.
Step 2: How to use the implemented CSE lib
tips
The
CARDCODE
,CARDVALIDITYDATE
andSELECTEDBRAND
parameters must respect specific formats. Please keep this in mind, especially when using validators, autospacing or autoformat scripts.
-
CARDCODE string(12-19)
The user’s bank card’s Primary Account Number (PAN).
Example: 1111222233334444
-
CARDCVV string(3-4)
The user’s bank card’s cryptogram.
Example: 123
-
CARDVALIDITYDATE date(MM-YY)
Card expiry date. In case of Network Token Transaction, provide Network Token Expiration Date
Example: 12-17
-
SELECTEDBRAND
cb
,visa
,vpay
,electron
,mastercard
,maestro
Preferred brand. (Please see the dedicated documentation)
Example: cb
<script type="text/javascript">
const cseLib = window.cseLib;
// Form submission function
function submitForm(e) {
let apiKeyId = "{Your API key here}";
let cseServiceUrl =
"https://sandbox-payment.dlns.io/cseKeys";
let postFormUrl =
"https://secure-test.dalenys.com/hosted-fields/service/tokenize";
e.preventDefault();
let jsonData = {
CARDCODE: document.getElementById("{card number input}").value,
CARDVALIDITYDATE: document.getElementById("{card expiry input}")
.value,
CARDCVV: document.getElementById("{card cryptogram input}").value,
SELECTEDBRAND: document.querySelector("{card brand input}:checked")
.value,
};
cseLib.preparePayload(cseServiceUrl, apiKeyId, jsonData).then(
(postPayLoad) => {
cseLib
.postEncryptedData(postFormUrl, postPayLoad)
.then((response) => {
if (response.error) {
// Notify user of the failed transaction
throw "Error : " + response.error;
}
/*
* Use the `response.HFTOKEN` in the request of the payment transaction
*/
});
},
(reason) => {
// Notify user of the failed transaction
throw reason;
}
);
}
</script>
Sending a payment request
Once the token has been received by your server-side script, you have to initiate a server-to-server request except for all the bank card data (CARDCODE
, CARDCVV
, CARDVALIDITYDATE
) which must be replaced by the token sent as a single TOKEN
parameter.
info
‘In case of usage of a brand selector, you have to add the selected brand to the form submit request (by adding an hidden input for example) and to add the dedicated
SELECTEDBRAND
parameter to your following payment request. See our live demo’
Here is a server to server request example:
$> curl --request POST --url "https://secure-test.dalenys.com/front/service/rest/process" \
--data "method=payment" \
--data "params[IDENTIFIER]=Demo Shop" \
--data "params[OPERATIONTYPE]=payment" \
--data "params[ORDERID]=1234" \
--data "params[AMOUNT]=1000" \
--data "params[CLIENTIDENT]=john.snow" \
--data "params[CLIENTEMAIL]=john.snow@example.com" \
--data "params[CLIENTREFERRER]=https://your_shop.com/order?id=1234" \
--data "params[CLIENTUSERAGENT]=Mozilla/5.0 (Windows NT 6.1; WOW64; rv:47.0) Gecko/20100101 Firefox/47.0" \
--data "params[CLIENTIP]=10.1.1.1" \
--data "params[CARDFULLNAME]=JOHN SNOW" \
--data "params[DESCRIPTION]=Knows nothing" \
--data "params[HFTOKEN]=17730892-b3f7-4411-bc81-557471ffcede" \
--data "params[HASH]=15477dcb8687adf90fa51e418f3c1a2d025f40b177a978c2734514734633b3c4" \
--data "params[VERSION]=3.0" \
info
The token is only available for one transaction and cannot be reused.
info
The token expires after 15 minutes.
Requirements
This JavaScript code is compatible with all recent desktop and mobile browsers (Edge, Chrome, Firefox, Safari…)
User experience on older browsers could be not optimal, some features may be disabled for technical or security motives.
SSL / TLS certificate
Please refer to the SSL / TLS certificate dedicated chapter.