Client-Side Encryption

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:

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.

Workflow

  1. You display a payment page including a payment form, implementing the encryption JavaScript code.
  2. At the submit process, you should request an encryption public-key (RSA-OAEP) from cseKeys provider service.
  3. Dalenys returns you the public-key.
  4. You encrypt the form values and send it to the tokenizer service.
  5. Dalenys returns you a token.
  6. 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.
  7. The Dalenys platform sends a request to the bank network and waits for the result.
  8. You receive the result in the request response.
  9. In parallel, the transaction result is confirmed by a notification request sent to the merchant’s NOTIFICATION_URL containing the transaction’s parameters (among which EXECCODE and TRANSACTIONID).
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://payment.dalenys.com/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-magenta1.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 and SELECTEDBRAND 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.

    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://payment.dalenys.com/cseKeys";
        let postFormUrl = "https://secure-magenta1.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.