Security
VodaPay Gateway is PCI DSS compliant. PCI DSS stands for Payment Card Industry Data Security Standard and is a PASA (Payment Association of South Africa) regulation in South Africa, this means any company accepting credit card payments on their website needs to comply in some way.
By using VodaPay Gateway means that you do not have to be concerned about being PCI compliant, and know that your card information is handled securely.
For more information on PCI compliance you can check out PCI Security Standards Council website.
Security at VodaPay Gateway
VodaPay Gateway forces HTTPS for all services using TLS 1.2, including all our APIs and Websites. For our Web Application Firewall we have partnered with Imperva.
Imperva protects VodaPay Gateway from:
- Bot Mitigations
- DDoS Attacks
All of our libraries connect to VodaPay Gateway servers over TLS and verify TLS certificates on each connection. We regularly perform audits of our implementation, including the certificates we serve, the certificate authorities we use, and the ciphers we support. Currently, we use DigiCert. We use HSTS to ensure that browsers interact with VodaPay Gateway only serves over HTTPS.
Signature
In order to construct the Signature element of the Assurance Data, the values of the following fields need to be concatenated together if present. Note that they must be concatenated in this specific order.
- Merchant ID
- Transaction Amount
- Currency Code
- Retrieval Reference Number
- Retrieval Reference Number Extended
- Session ID
- Merchant's Private Key
This produces a concatenated string. Change this string to lowercase. Then generate a SHA512 hash value from the string. This hash value must be used as the Signature element within the assurance data payload JSON message, which is then encrypted into Assurance Data.
The Payment Gateway Server will also generate a Signature from the transaction data using the same method, and confirm that the two Signature values match. This provides a way of verifying that the message had not been tampered with.
Example
In the example message shown above, the transaction data used for the Signature would be the following:
- Merchant ID: VPS999999999999
- Transaction Amount: 50000
- Currency Code: 710
- Retrieval Reference Number: 348934893489
- Retrieval Reference Number, Extended: MERCH383483489
- Session ID: 26bcbeef-c5c1-4313-8b11-4730fc6a07ab
- Merchant's Private Key: 02225271-595a-4378-bf2c-8da81e29c195
Result
With this source data, the Signature would be generated as follows:
1. Concatenate all values: VPS99999999999950000710348934893489MERCH38348348926bcbeef-c5c1-4313-8b11-4730fc6a07ab02225271-595a-4378-bf2c8da81e29c195
2. Change all to lower case: vps99999999999950000710348934893489merch38348348926bcbeef-c5c1-4313-8b11-4730fc6a07ab02225271-595a-4378-bf2c-8da81e29c195
3. Generate a SHA512 hash value: D63F6C9C56C1EDB20CC0E110F7F4D5BF439E879EC9D2F80FAC69A48B9EE8311EEF547DC3F30BBC6B79CA8A92A72570754AF9BFF5FE42918496149B382CAB71E2
Assurance Data
Assurance Data is a data element that the integrating e-commerce system embeds in its request messages when sending them to the VodaPay Gateway. It provides a guarantee that the customer has been successfully authenticated by the integrating system. NB. Digital wallet ID is not part of the payload on the Refunds endpoint call. Note that Assurance Data is a mandatory element in all such request messages sent from the integrating system to the PSP-Acquirer. Assurance Data is typically generated once the customer logs in and is authenticated, and remains unchanged until the session ends.
The Assurance Data element consists of binary data and can be up to 2000 characters in length. It is composed of a 16-byte initialisation vector, which is randomly generated each time, followed by an encrypted data block.
The cipher text of the encrypted data block is generated by using AES/CBC/PKCS7Padding encryption and the merchant private key to encrypt an assurance data payload. The merchant private key is provided by VodaPay Gateway during the initial setup as a UUID value, from which the dashes need to be removed to make it a 256-bit key.
The assurance data payload needs to consist of the following four elements in a JSON message:
- The timestamp of the request message, which is an integer value representing the number of milliseconds since 00:00 UTC on 1 January 1970.
- The Digital Wallet Identifier, which is a value in UUID format that identifies the transacting customer. It is constructed by prefixing a standard string onto the customer's digital identity and then using a UUID generator to generate a UUID value from this concatenated string.
- The Signature, which is used to confirm that the message received was not tampered with. This is explained in further detail below.
- The Session Identifier, which is a value in UUID format that identifies the session. This would be the same value for all messages relating to all transactions conducted by a customer during a single authenticated session.
Code Example: Build and encrypt assurance data
The following example shows the composition of the assurance data payload before encryption into cipher text:
- Request
- Node.js
- C#
- Java
Request
{
"timestamp": 1598432046000,
"digitalWalletId": "a97cb91f-473f-41fd-8f3a-086fc0e10e70",
"signature": "2CE9F45638AD9452D85E763472939613916547ADF262874CE20E3BE500F5F9D4AEDD5AA9DD5CC54354EB8801DABE5C769D44F162FFC2AE17E78177F1D2416F99",
"sessionId": "00000000-0000-0000-0000-000000000000"
}
Node.js
In the below example you would simply call the function createAssuranceData with the relevant parameters.
const crypto = require("crypto");
function getFormattedDate() {
const currentDate = new Date();
const year = currentDate.getFullYear();
const month = String(currentDate.getMonth() + 1).padStart(2, "0");
const day = String(currentDate.getDate()).padStart(2, "0");
const hours = String(currentDate.getHours()).padStart(2, "0");
const minutes = String(currentDate.getMinutes()).padStart(2, "0");
const seconds = String(currentDate.getSeconds()).padStart(2, "0");
const milliseconds = String(currentDate.getMilliseconds()).padStart(3, "0");
const formattedDate = `${year}${month}${day}${hours}${minutes}${seconds}${milliseconds}`;
return formattedDate;
}
function encrypt(text, key) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
let encrypted = cipher.update(text, "utf8", "base64");
encrypted += cipher.final("base64");
const extraBytesBuffer = Buffer.from(iv);
const encryptedBuffer = Buffer.from(encrypted, "base64");
const combinedBuffer = Buffer.concat([extraBytesBuffer, encryptedBuffer]);
return new Uint8Array(combinedBuffer);
}
function createAssuranceData(
sessionId,
merchantId,
digitalWalletId,
amount,
retrievalReferenceNumber,
retrievalReferenceNumberExtended,
merchantPrivateKey
) {
const currencyCode = "720";
let signature = (
merchantId +
amount +
currencyCode +
retrievalReferenceNumber +
retrievalReferenceNumberExtended +
sessionId +
merchantPrivateKey
).toLowerCase();
const sha512Hash = crypto.createHash("sha512");
signature = sha512Hash.digest("base64");
const timestamp = getFormattedDate();
const assuranceData =
'{"timestamp":"' +
timestamp +
'","digitalWalletId": "' +
digitalWalletId +
'","signature":"' +
signature +
'","sessionId":"' +
sessionId +
'"}';
const key = merchantPrivateKey.toLowerCase().replaceAll("-", "");
const encryptedBuffer = Buffer.from(encrypt(assuranceData, key));
return encryptedBuffer.toString("base64");
}
C#
In the below example you would simply call the function CreateAssuranceData with the relevant parameters.
using System.Text.Json;
using System.Security.Cryptography;
using System.Text;
static string SHA512(string text)
{
var result = string.Empty;
var d = Encoding.UTF8.GetBytes(text);
using SHA512 a = new SHA512Managed();
var h = a.ComputeHash(d);
result = BitConverter.ToString(h).Replace("-", "");
return result;
}
static string AesEncrypt(string inputText, string keyString)
{
byte[] cipherData;
var inputBytes = UTF8Encoding.UTF8.GetBytes(inputText);
var aes = Aes.Create();
aes.KeySize = 256;
aes.Key = Encoding.UTF8.GetBytes(keyString);
aes.GenerateIV();
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
var cipher = aes.CreateEncryptor(aes.Key, aes.IV);
using var memoryStream = new MemoryStream();
using var cryptoStream = new CryptoStream(memoryStream, cipher, CryptoStreamMode.Write);
cryptoStream.Write(inputBytes, 0, inputBytes.Length);
cryptoStream.FlushFinalBlock();
cipherData = memoryStream.ToArray();
var combinedData = new byte[aes.IV.Length + cipherData.Length];
Array.Copy(aes.IV, 0, combinedData, 0, aes.IV.Length);
Array.Copy(cipherData, 0, combinedData, aes.IV.Length, cipherData.Length);
return Convert.ToBase64String(combinedData);
}
static string CreateAssuranceData(string sessionId, string merchantId, string? digitalWalletId, decimal? amount, string retrievalReferenceNumber, string retrievalReferenceNumberExtended, string merchantPrivateKey, bool noSignature = false)
{
var currencyCode = "710";
var signature =
$"{merchantId}{amount}{currencyCode}{retrievalReferenceNumber}{retrievalReferenceNumberExtended}{sessionId}{merchantPrivateKey}".ToLower();
signature = SHA512(signature);
object assuranceDataObject;
var timestamp = DateTimeOffset.Now.ToString("yyyyMMddHHmmssffff");
digitalWalletId = !string.IsNullOrEmpty(digitalWalletId) ? digitalWalletId : null;
if (noSignature)
{
assuranceDataObject = new
{
timestamp,
digitalWalletId,
sessionId
};
}
else
{
assuranceDataObject = new
{
timestamp,
digitalWalletId,
signature,
sessionId
};
}
var assuranceDataString = JsonSerializer.Serialize(assuranceDataObject);
var key = merchantPrivateKey.ToLower().Replace("-", "");
return AesEncrypt(assuranceDataString, key);
}
Java
In the below example you would simply call the function createAssuranceData with the relevant parameters.
import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.security.SecureRandom;
public static String calcHash(String input) throws NoSuchAlgorithmException {
MessageDigest sha512 = MessageDigest.getInstance("SHA-512");
byte[] hashedBytes = sha512.digest(input.getBytes());
StringBuilder hexStringBuilder = new StringBuilder(2 * hashedBytes.length);
for (byte b : hashedBytes) {
hexStringBuilder.append(String.format("%02x", b));
}
return hexStringBuilder.toString();
}
public static byte[] generateIV() {
final int IV_LENGTH = 16;
SecureRandom random = new SecureRandom();
byte[] iv = new byte[IV_LENGTH];
do {
random.nextBytes(iv);
} while (iv[0] == 0);
return iv;
}
public static String encrypt(String data, String key) throws Exception {
byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
byte[] ivBytes = generateIV();
SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, "AES");
IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivSpec);
byte[] byteData = data.getBytes(StandardCharsets.UTF_8);
byte[] encryptedData = cipher.doFinal(byteData);
byte[] combined = new byte[ivBytes.length + encryptedData.length];
System.arraycopy(ivBytes, 0, combined, 0, ivBytes.length);
System.arraycopy(encryptedData, 0, combined, ivBytes.length, encryptedData.length);
return Base64.getEncoder().encodeToString(combined);
}
public static String createAssuranceData(String sessionId, String merchantId, String digitalWalletId, Optional<BigDecimal> amount, String retrievalReferenceNumber, String retrievalReferenceNumberExtended, String merchantPrivateKey) throws Exception {
Currency defaultCurrency = Currency.getInstance(Locale.getDefault());
int CurrencyCode = defaultCurrency.getNumericCode();
String signature = (merchantId + amount.get() + CurrencyCode + retrievalReferenceNumber + retrievalReferenceNumberExtended + sessionId + merchantPrivateKey).toLowerCase();
signature = calcHash(signature);
LocalDateTime currentDateTime = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSSS");
String timestamp = currentDateTime.format(formatter);
String assuranceData = "{\"timestamp\":\"" + timestamp + "\",\"digitalWalletId\": \"" + digitalWalletId + "\",\"signature\":\"" + signature + "\",\"sessionId\":\"" + sessionId + "\"}";
String key = merchantPrivateKey.toLowerCase().replace("-", "");
return encrypt(assuranceData, key);
}
NB. Digital wallet ID is not part of the payload on the Refunds endpoint call.
Code Example: Decrypt assurance data
- Node.js
- C#
- Java
Node.js
In the below example you would simply call the function unlockAssuranceData with the relevant parameters.
const crypto = require("crypto");
function decrypt(data, key) {
const algorithm = "aes-256-cbc";
const iv = crypto.randomBytes(16);
const decipher = crypto.createDecipheriv(algorithm, key, iv);
let decryptedData = decipher.update(data, "base64", "utf-8");
decryptedData += decipher.final("utf8");
return decryptedData;
}
function unlockAssuranceData(assuranceData, key) {
const privateKey = key.toLowerCase().replaceAll("-", "");
const decryptedData = decrypt(assuranceData, privateKey);
return decryptedData.substring(decryptedData.indexOf("{"));
}
C#
In the below example you would simply call the function UnlockAssuranceData with the relevant parameters.
using System.Security.Cryptography;
using System.Text;
static string AesDecrypt(string inputText, string keyString)
{
var inputBytes = Convert.FromBase64String(inputText);
var keyBytes = Encoding.UTF8.GetBytes(keyString);
var aes = Aes.Create();
aes.GenerateIV();
aes.KeySize = 256;
aes.Key = keyBytes;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
var cipher = aes.CreateDecryptor(aes.Key, aes.IV);
using var memoryStream = new MemoryStream(inputBytes);
using var cryptoStream = new CryptoStream(memoryStream, cipher, CryptoStreamMode.Read);
using var srDecrypt = new StreamReader(cryptoStream);
return srDecrypt.ReadToEnd();
}
static string UnlockAssuranceData(string assuranceData, string merchantPrivateKey)
{
var assuranceDataString = AesDecrypt(assuranceData, merchantPrivateKey.ToLower().Replace("-", ""));
return assuranceDataString.Substring(assuranceDataString.IndexOf("{"));
}
Java
In the below example you would simply call the function unlockAssuranceData with the relevant parameters.
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Base64;
public static byte[] generateIV() {
final int IV_LENGTH = 16;
SecureRandom random = new SecureRandom();
byte[] iv = new byte[IV_LENGTH];
do {
random.nextBytes(iv);
} while (iv[0] == 0);
return iv;
}
public static String decrypt(String data, String key) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
byte[] ivBytes = generateIV();
SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, "AES");
IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivSpec);
byte[] byteData = Base64.getDecoder().decode(data);
byte[] decryptedData = cipher.doFinal(byteData);
return new String(decryptedData);
}
public static String unlockAssuranceData(String assuranceData, String key) throws InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException {
key = key.toLowerCase().replace("-", "");
String decryptedString = decrypt(assuranceData,key);
decryptedString = decryptedString.substring(decryptedString.indexOf('{'));
return decryptedString;
}