Client-Side Encryption
Encrypt sensitive card information in the browser before sending it to Cycle servers to reduce your risk and PCI compliance scope.
Overview
Client-side encryption (CSE) involves fetching a public key from Cycle, encrypting card data on the client (e.g., in the browser), and then submitting the encrypted data to the Charge API. This ensures that sensitive card information never touches your servers directly.
Step 1: Fetch the Encryption Key
API Endpoint
GET /api/v3/configuration/encryptionkeyAuthentication Required
Query Parameters
| Field | Required | Description |
|---|---|---|
format | Yes | The format of the key to return. Supported values: JWK, JAVA. |
forceNew | No | Force generation of a new key. Optional values: true, false. Defaults to false. |
Example Request
curl 'https://sandbox.cyclepay.net/api/v3/configuration/encryptionkey?format=JWK' \
--header 'X-MerchantAccount: CycleDemo' \
--header 'X-CallerName: cycle-api-caller' \
--header 'X-HMAC-Timestamp: 1633767872' \
--header 'X-HMAC-Signature: 2A5B9C3D...' \
--header 'Content-Type: application/json'Response Parameters
| Field | Description |
|---|---|
kty | For JWK format. Fixed to “RSA” |
extractable | For JWK format. Fixed to “true” |
n | For JWK format. The modulus of the public key, Base64Url encoded. |
e | For JWK format. The exponent of the public key, Base64Url encoded. |
key | For “JAVA” format, the public key in Base64 encoded X.509 format. |
Example Response (JWK)
{
"kty": "RSA",
"extractable": true,
"n": "qHMEj_ErzxavCP56nUyhGaqPwAkfTPy1wVwHMUZvpWxGoDTXHi6K8vWH6heLM4vDPL5Gxk1Wlq1uLwd0yd4GfYjBol5EyQayKZQe-ajxFDOrE3BzabZNHc5K4guPfamQCO6YnrrO1CG5GtLr75pRvd7FZErCd42UXxsq2GBSt3QfAtzbKOK3-5QBr-CFndYy3Yr6gnV-5lixIvnsRlQIP-882fAgN_GcOFA5iHuhCTDjDgpOMt9eTj9TGskpDdW19vjtBDG45RDDAF6-ZxCrn7jk-5Q_LlgLqIpvqzmswlFTAtxKANErNxWW9_MVYsRB1XkqvWOglJaY1ZPO7XCfpQ",
"e": "AQAB"
}Example Response (Java)
{
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqHMEj/ErzxavCP56nUyhGaqPwAkfTPy1wVwHMUZvpWxGoDTXHi6K8vWH6heLM4vDPL5Gxk1Wlq1uLwd0yd4GfYjBol5EyQayKZQe+ajxFDOrE3BzabZNHc5K4guPfamQCO6YnrrO1CG5GtLr75pRvd7FZErCd42UXxsq2GBSt3QfAtzbKOK3+5QBr+CFndYy3Yr6gnV+5lixIvnsRlQIP+882fAgN/GcOFA5iHuhCTDjDgpOMt9eTj9TGskpDdW19vjtBDG45RDDAF6+ZxCrn7jk+5Q/LlgLqIpvqzmswlFTAtxKANErNxWW9/MVYsRB1XkqvWOglJaY1ZPO7XCfpQIDAQAB"
}Step 2: Encrypt Card Info & Calculate Hash
A. Get the Public Key Byte Array
For JAVA format, Base64-decode the `key`. For JWK, decode the modulus (`n`) and exponent (`e`) to build the RSA public key.
import java.security.spec.RSAPublicKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.math.BigInteger;
// springframework.util.Base64Utils used for decoding
public byte[] getPublicKeyBytes(EncryptionKeyResponse response) throws Exception {
if (response.getKey() != null) { // JAVA Format
return Base64Utils.decode(response.getKey().getBytes());
} else { // JWK Format
BigInteger modulus = new BigInteger(1, Base64Utils.decodeFromUrlSafeString(response.getN()));
BigInteger exponent = new BigInteger(1, Base64Utils.decodeFromUrlSafeString(response.getE()));
PublicKey pub = KeyFactory.getInstance("RSA").generatePublic(new RSAPublicKeySpec(modulus, exponent));
return pub.getEncoded();
}
}B. Encrypt the Sensitive Data
Use the public key byte array and OAEP with SHA-256 padding to encrypt fields like `cardNumber` and `securityCode`. The result should be Base64-encoded.
import javax.crypto.Cipher;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
// BouncyCastle provider required: "BC"
public static String encryptWithPublicKey(String clearText, byte[] encodedPublicKey) {
try {
X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(encodedPublicKey);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(publicKeySpec);
Cipher cipher = Cipher.getInstance("RSA/NONE/OAEPWITHSHA-256ANDMGF1PADDING", "BC");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] encryptedBytes = cipher.doFinal(clearText.getBytes());
return Base64Utils.encodeToString(encryptedBytes);
} catch (Exception e) {
throw new RuntimeException("Encryption failed", e);
}
}C. Calculate the encryptionKeyHash
Create a SHA-256 hash of the raw public key bytes (before it's encoded into JWK or JAVA string format) and then Base64-encode the hash digest.
- JAVA format:
encryptionKeyHash = Base64.encode(sha256(Base64.decode(response.key))) - JWK format:
encryptionKeyHash = Base64.encode(sha256(Base64.decodeUrl(response.n)))
import java.security.MessageDigest;
private String sha256Hash(byte[] keyBytes) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(keyBytes);
return Base64Utils.encodeToString(hash);
} catch (Exception e) {
throw new RuntimeException("Could not calculate SHA-256 hash", e);
}
}
public String getPublicKeyHash(EncryptionKeyResponse response) throws Exception {
if (response.getKey() != null) { // JAVA Format
return sha256Hash(Base64Utils.decode(response.getKey().getBytes()));
} else { // JWK Format
return sha256Hash(Base64Utils.decodeFromUrlSafeString(response.getN()));
}
}Step 3: Submit the Payment Request
Submit the charge request with the encrypted values and two additional headers: X-KeyHash and X-Sensitive. These headers must also be included in your HMAC signature calculation.
Additional Headers
| Header | Required | Description |
|---|---|---|
X-KeyHash | Yes | The calculated “encryptionKeyHash” from Step 2. |
X-Sensitive | No | Comma-separated list of encrypted fields. Defaults to “cardNumber,securityCode”. |
Updated HMAC Signature Message
timestamp = seconds_from_epoch_utc
message = string_concat(callerName, merchantAccountName, keyHash, sensitive, timestamp, request_path, request_body)
signature = hmac_sha256_as_hexadecimal(api_secret, message)Example Charge Request (with encrypted data)
{
"command": "CHARGE",
"id": "txn_cse_demo_001",
"customer": {
"id": "TestUserCSE"
},
"lineItem": {
"currency": "ZAR",
"amount": 14.34,
"description": "CSE Demo Product"
},
"payment": {
"paymentMethod": "VISA",
"countryCode": "ZA",
"accountHolder": "Card Holder",
"cardNumber": "gf01ekNx1LR4CeSj//B5TvTNKcVg5GPWFt09UI3rotYNddXGfyb8uL2TQJuqJLiAFT6It+BIoJLQaef7JGGf3gLnfYOIhWIO2I6gUn1KN5zX8AcSDDHeuIHua2602nVBhx02/Zs7N4eBKBAOQBdaBvbdP9diYX9aztoWw6TU74YR3o9DoOoNKZC+yWPL9UbFfVTk5j85pAmMh50rnV1x8aAnyj43CUhGhKJnvWJoZSSnfpq309WfTDZZCz539d8DQbkJA8pW3z5sg8QNCR0iw8HYIDslnro6cq3EMdyPNKNIHMZBYCJC4YI0vrCkUdMn+4Q5DnHKo5oLNs/o8htcIw==",
"expirationDate": "2023-06",
"securityCode": "bJmMgiYzImTzk+c+5l+QcghK8XGZPgY7K7T3dJHInS2eZrWZqr14ek9REoy4thzXfPLapJv7gQY3sf0g8t8NndAHvUubV4oE3mZ1jwWV8V8PPPQxvFGvFk1OsIauiY+ffWYcR1jx+08gB11+iX7ieaz0QM+T9NchRgHgKnYBpqssnKMsAmw7hjWL+L396YKn6ytJZDrwWb8ghE2IKV6eaTrENCh459tq2BiSdLtYqhBeGl4yiizz2R0iY8y8npRSHUe5wYIIcjLilK1zcWmNALqB9df1TuIvHwE1J8KtN5ULc5Bcnu3viHHroON+yGMGok/aKzqdUgxvzUWlcoog9w=="
}
}The server will decrypt the sensitive fields and process the payment. The response will be the same as a standard charge response, but the sensitive fields will not be included.