Encryption in LWC Salesforce with CryptoJS A Step-by-Step Guide to Securely Masking and Encrypting Credit Card Data

Encryption in Salesforce Lightning Web Component (LWC) with CryptoJS: A Step-by-Step Guide to Securely Masking and Encrypting Credit Card Data

When handling sensitive information in Salesforce, encryption in LWC with CryptoJS is essential for protecting data like credit card numbers. In this post, we’ll guide you step-by-step on implementing encryption in LWC with CryptoJS. This will help you securely mask and encrypt credit card data before sending it to Apex. This ensures complete security for your customer information.

Encrypting data directly in the Lightning Web Component (LWC) offers an extra layer of security. We’ll use JavaScript to mask the credit card number for user display. After masking, we will encrypt it before securely sending it to the Apex server.

Why Use Encryption in Salesforce?

Client-side encryption is beneficial for several reasons:

  • Sensitive Data Protection: Sensitive data protection is crucial. Masking the credit card number enhances privacy. Meanwhile, encryption keeps the data secure even before it reaches Apex.
  • Compliance: Regulatory requirements often mandate encryption for sensitive data like payment information.
  • End-to-End Security: End-to-end security is vital. Encrypt data in the LWC to protect it from the moment users enter it. This protection continues until the data processes on your server or a third-party service.

This example shows how to handle credit card data securely in a Salesforce Lightning Web Component (LWC). We will mask the data on ui and encrypt it before sending it to the Apex server. This solution addresses the following business scenario:

Imagine a sales or support team collecting customer credit card information in Salesforce for payment processing. Due to the sensitive nature of credit card data, ensure only authorized systems can read it. Mask the data when displayed in the UI to protect user privacy.

Here’s a summary of the steps involved in this component:

  1. Credit Card Type Selection: Users select a credit card type, such as Visa, MasterCard, AMEX, or Discover etc. This choice determines the card number length and the appropriate masking pattern.
  2. Client-Side Masking: After users input the credit card number, the component masks it on blur. It displays only the last few digits. For AMEX cards, it shows the last 3 digits, while for other card types, it displays the last 4 digits.
  3. Client-Side Encryption: The card number is encrypted on the client side using the CryptoJS library. An AES encryption key is provided by Apex. Prepare this encrypted data for secure transmission to the server, allowing storage or processing without exposing sensitive details.

Benefits of This Approach

This LWC protects sensitive credit card information on the user interface through masking and during transmission through encryption. By encrypting data client-side, it maintains confidentiality. It protects against unauthorized access. Additionally, it complies with security standards for handling sensitive payment information.

Let’s examine the specific code used to implement this functionality. We will highlight the key functions that perform the masking and encryption steps.

Demo:

Step 1: Set Up Crypto JS in Salesforce for JavaScript Encryption

For using encryption in LWC with CryptoJS in JavaScript, you can use the crypto-js library. Follow these steps to upload and use it as a static resource in your Salesforce org:

  1. Download crypto-js: Click Here cryptoJS to download the library.
  2. Upload to Salesforce: In Salesforce Setup, go to Static Resources and upload the crypto-js library file as cryptoJS. Choose the Cache Control setting as Public.

Step 2: Build the LWC for Capturing, Masking, and Encrypting Credit Card Data

<!–HTML File: Data Input and Submission Button–>
<!-In the following code, we create an LWC that masks the credit card number on the UI.
It encrypts the actual value before sending it to Apex.–>
<template>
<lightning-card title="Encryption In LWC">
<div class="slds-m-around_medium">
<lightning-combobox name="cardType" label="Card Type" placeholder="Please select…"
options={cardTypes} onchange={handleChange} required>
</lightning-combobox>
<lightning-input name="cardNumber" type="text" label="Card Number" min-length="15" max-length="16"
onfocus={handleFocus} data-id="cardNumber" onblur={handleBlur} onchange={handleChange}
value={displayValue} required> </lightning-input><br/>
<lightning-button label="Submit" onclick={handleSubmit}></lightning-button>
</div>
</lightning-card>
</template>
/*JavaScript (JS) File: Handling Masking and Encryption
In this file, we mask the credit card number when it loses focus. We encrypt it before sending it to Apex.*/
// Import necessary modules and decorators from the LWC framework.
import { LightningElement, wire } from 'lwc';
// Utility function to load external JavaScript libraries dynamically.
import { loadScript } from 'lightning/platformResourceLoader';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
// Import the CryptoJS library from Salesforce static resources using its resource URL.
import CryptoJS from '@salesforce/resourceUrl/cryptoJS';
// Import the Apex method to fetch the AES encryption key from the server.
import getAESKey from '@salesforce/apex/EncryptionCtrl.getAESKey';
// Import the Apex method to send encrypted data back to the server.
import sendEncryptedDataToApex from '@salesforce/apex/EncryptionCtrl.sendEncryptedData';
export default class EncryptionExample extends LightningElement {
// Property to hold the actual value of the credit card number entered by the user.
actualValue = '';
// Property to hold the masked version of the credit card number for display purposes.
maskedValue = '';
// Property to hold the value that will be displayed to the user,
//which may be either the masked or actual value.
displayValue = '';
/* Boolean flag to indicate if the input is invalid
(e.g., if the credit card number format is incorrect).*/
isInvalid = false;
/* Variable to hold the reference to a timeout for
input delay handling (e.g., for debouncing).*/
delayTimeout;
// Property to store the selected type of credit card (e.g., Visa, MasterCard).
selectedCardType;
// Property to hold the encryption key fetched from the Apex method.
encryptionKey;
// Property to hold the encrypted data before it is sent to the server.
encrypted;
// Array to define the different types of credit cards supported, each with a label and value.
cardTypes = [
{ label: 'Visa', value: 'Visa' }, // Visa card option.
{ label: 'Master Card', value: 'Mastercard' }, // MasterCard option.
{ label: 'Amex', value: 'AMEX' }, // American Express (Amex) option.
{ label: 'Discover', value: 'Discover' } // Discover card option.
];
/*
* @Description : connectedCallback method is executed when the component is inserted into the DOM.
* It loads the CryptoJS script, ensuring that the library is available for use
in the component. Upon successful loading, it logs a confirmation message,
and in case of an error, it logs the error details.
* @return – N.A.
*/
connectedCallback() {
// Load CryptoJS script once the component is rendered.
Promise.all([loadScript(this, CryptoJS)])
.then(() => console.log('CryptoJS loaded'))
.catch(error => console.error('Error loading CryptoJS:', error));
}
//using the @wire decorator to connect to the getAESKey Apex method
@wire(getAESKey)
/*
* @Description : AESKeyHandler method processes the data returned from the Apex method
connected via the @wire decorator. It assigns the retrieved encryption key
to the component's state and logs the key to the console. In case of an
error, it logs the error details.
* @param {Object} data – the data returned from the Apex method, representing the encryption key.
* @param {Object} error – the error object returned from the Apex method, if any error occurs.
* @return – N.A.
*/
AESKeyHandler({ error, data }) {
// Check if the data object exists, meaning the key was fetched successfully.
if (data) {
// Assign the received encryption key to the class property for further use.
this.encryptionKey = data;
// Log the received key for debugging purposes.
console.log('Received Key:', this.encryptionKey);
// Check if there was an error during the data retrieval process.
} else if (error) {
// Log the error details for debugging purposes.
console.error('Error fetching key:', error);
}
}
/*
* @Description : handleChange method is used to manage user input for credit card type and number fields.
* When the credit card type is selected, it resets the display, masked, and actual values.
* For the card number field, it validates and formats the input based on the selected card type,
applies a masking pattern if the input length matches the required max length (15 for AMEX, 16 for others),
and encrypts the number after a 500ms delay if no further typing is detected.
* @param – event – the event object from the input field, used to determine which field triggered the change.
* @return – N.A.
*/
handleChange(event) {
const fieldName = event.target.name;
if (fieldName === 'cardType') {
this.selectedCardType = event.target.value;
this.displayValue = '';
this.maskedValue = '';
this.actualValue = '';
} else if (fieldName === 'cardNumber') {
// Clear previous timer if user is still typing
window.clearTimeout(this.delayTimeout);
try {
const inputValue = event.target.value.replace(/\D/g, '');
this.actualValue = inputValue;
const cardType = this.selectedCardType;
const maxLength = cardType === 'AMEX' ? 15 : 16;
if (inputValue.length === maxLength) {
this.maskedValue = this.applyMaskOnCardNo(inputValue, cardType);
this.displayValue = this.maskedValue;
this.isInvalid = false;
} else {
this.displayValue = inputValue;
this.isInvalid = inputValue.length < maxLength;
}
// Delay the encryption and validation until user stops typing for 300ms
this.delayTimeout = setTimeout(() => {
this.encrypted = this.encryptCardNoWithAES(this.actualValue, this.encryptionKey);
console.log('Encrypted+++', this.encrypted);
}, 500);
} catch (error) {
console.log('Error:', JSON.stringify(error));
}
}
}
/*
* @Description : handleFocus method is triggered when the card number input field gains focus.
* It formats the card number for display if the actual value length matches the
required max length (15 for AMEX, 16 for other card types).
* @param – event – the event object from the input field, used to access the field and format
the value for display based on the selected card type.
* @return – N.A.
*/
handleFocus(event) {
try {
const input = event.target;
const cardType = this.selectedCardType;
const maxLength = cardType === 'AMEX' ? 15 : 16;
if (this.actualValue.length === maxLength) {
input.value = this.formatCardNoForDisplay(this.actualValue, cardType);
}
} catch (error) {
console.log('Error:', error);
}
}
/*
* @Description : handleBlur method is triggered when the card number input field loses focus.
* It applies a masked format to the card number if the actual value length matches
the required max length (15 for AMEX, 16 for other card types);
otherwise, it displays the unmasked actual value. It also sets the display
value and verifies the card number validity.
* @param – event – the event object from the input field, used to set and display the formatted
or unmasked value and check for validity.
* @return – N.A.
*/
handleBlur(event) {
try {
const input = event.target;
const cardType = this.selectedCardType;
const maxLength = cardType === 'AMEX' ? 15 : 16;
if (this.actualValue.length === maxLength) {
this.maskedValue = this.applyMaskOnCardNo(this.actualValue, cardType);
input.value = this.maskedValue;
} else {
input.value = this.actualValue;
}
this.displayValue = input.value;
this.checkCardNoValidity();
} catch (error) {
console.log('Error:', error);
}
}
/*
* @Description : checkCardNoValidity method validates the credit card number input fields.
* If the card number length does not match the required
max length (15 for AMEX, 16 for other card types),
it sets a custom validity message indicating the required number of digits.
If valid, it clears any custom validity messages.
The method then reports validity for the first input field.
* @param – N.A.
* @return – N.A.
*/
checkCardNoValidity() {
const inputFields = this.template.querySelectorAll('lightning-input[data-id="cardNumber"]');
if(inputFields !== null && inputFields != undefined) {
console.log('inputFields+++'+inputFields.length);
console.log('this.isInvalid+++'+this.isInvalid);
inputFields.forEach(inputField => {
if (this.isInvalid) {
const cardType = this.selectedCardType;
const maxLength = cardType === 'AMEX' ? 15 : 16;
inputField.setCustomValidity(`You are required to enter ${maxLength} digits.`);
} else {
inputField.setCustomValidity('');
}
});
inputFields[0].reportValidity();
}
}
/*
* @Description : applyMaskOnCardNo method applies a masking format
to the credit card number based on the card type.
* For AMEX cards, it masks all but the last 3 digits (e.g., XXXXX-XXXXX-X123). For non-AMEX
cards, it masks all but the last 4 digits (e.g., XXXX-XXXX-XXXX-1234).
* @param – value – the actual credit card number to be masked.
* @param – cardType – the type of credit card (e.g., 'AMEX'), which determines the masking format.
* @return – string – the masked credit card number.
*/
applyMaskOnCardNo(value, cardType) {
if (cardType === 'AMEX') {
return `XXXXX-XXXXX-X${value.slice(11)}`; // Last 3 digits for AMEX
} else {
return `XXXX-XXXX-XXXX-${value.slice(12)}`; // Last 4 digits for non-AMEX
}
}
/*
* @Description : formatCardNoForDisplay method formats the credit card number
for display based on the card type.
* For AMEX cards, it groups the number into sections of 5-5-5 digits (e.g., 12345-67890-12345).
* For non-AMEX cards, it formats the number into sections of 4-4-4-4 digits
(e.g., 1234-5678-9012-3456).
* @param – value – the actual credit card number to be formatted.
* @param – cardType – the type of credit card (e.g., 'AMEX'), which determines the formatting pattern.
* @return – string – the formatted credit card number for display.
*/
formatCardNoForDisplay(value, cardType) {
if (cardType === 'AMEX') {
return `${value.slice(0, 5)}-${value.slice(5, 10)}-${value.slice(10)}`; // Format for AMEX
} else {
return `${value.slice(0, 4)}-${value.slice(4, 8)}-${value.slice(8, 12)}-${value.slice(12)}`; // Format for non-AMEX
}
}
/*
* @Description : decodeBase64ToBuffer method converts a base64-encoded string into an ArrayBuffer.
* It decodes the base64 string into a binary string, then creates a Uint8Array
to store each character's byte value, and finally returns the ArrayBuffer.
* @param – base64 – the base64-encoded string to be converted.
* @return – ArrayBuffer – an ArrayBuffer containing the decoded byte data.
*/
decodeBase64ToBuffer(base64) {
// `atob` is a built-in JavaScript function that decodes a base64 encoded string.
// It takes a base64 string and returns a binary string (a string where each character represents a byte).
// Decode the base64 encoded string to get a binary string representation
const binaryString = atob(base64);
// Get the length of the binary string, which represents the number of bytes
const len = binaryString.length;
// `Uint8Array` is a typed array in JavaScript that represents an array of 8-bit unsigned integers.
// Each element in the array holds a value between 0 and 255, which is suitable for storing binary data.
// Create a new Uint8Array (an array of 8-bit unsigned integers) with the same length as the binary string
const bytes = new Uint8Array(len);
// Loop through each character in the binary string
for (let i = 0; i < len; i++) {
// `charCodeAt` is a method that returns the Unicode value (byte value in this case) of the character at the specified index.
// Here, we're converting each character in the binary string to its byte value and storing it in the `Uint8Array`.
bytes[i] = binaryString.charCodeAt(i);
}
// `bytes.buffer` gives us the underlying `ArrayBuffer` of the `Uint8Array`.
// An `ArrayBuffer` is a low-level binary data structure that stores raw binary data in memory.
// Return the underlying ArrayBuffer, which is a binary buffer that holds the data in memory
return bytes.buffer;
}
/*
* @Description : combineBuffers method combines two ArrayBuffers into a single ArrayBuffer.
* It creates a new Uint8Array with a length equal to the sum of the byte lengths
* of the two input buffers, copies the contents of both buffers into the new array,
* and returns the resulting ArrayBuffer.
* @param – buffer1 – the first ArrayBuffer to be appended.
* @param – buffer2 – the second ArrayBuffer to be appended.
* @return – ArrayBuffer – a new ArrayBuffer containing the combined data of both input buffers.
*/
combineBuffers(buffer1, buffer2) {
// Create a new Uint8Array to hold the combined length of both buffers.
// `buffer1.byteLength` and `buffer2.byteLength` give us the size of each buffer in bytes.
const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
// `set` is a method of the Uint8Array that copies values from another array (or another typed array)
// into the current array starting at the specified offset (0 in this case).
// Here, we're creating a new Uint8Array from `buffer1` and copying it into the `tmp` array starting at index 0.
tmp.set(new Uint8Array(buffer1), 0);
// Again using the `set` method to copy values from `buffer2`.
// This time, we specify the starting index as `buffer1.byteLength`, which is where the second buffer will be placed
// immediately after the contents of `buffer1` in the `tmp` array.
tmp.set(new Uint8Array(buffer2), buffer1.byteLength);
// Return the underlying ArrayBuffer of the combined Uint8Array.
// `tmp.buffer` provides a single ArrayBuffer that contains the data from both input buffers.
return tmp.buffer;
}
/*
* @Description : encodeArrayBufferToBase64 method converts an ArrayBuffer into a base64-encoded string.
* It creates a Uint8Array from the input buffer, reduces it to a string by
converting each byte to its character representation, and then encodes that
string in base64 format using btoa.
* @param – arrayBuffer – the ArrayBuffer to be converted to a base64 string.
* @return – string – the base64-encoded representation of the input ArrayBuffer.
*/
encodeArrayBufferToBase64(arrayBuffer) {
// `Uint8Array` is used to create a typed array from the provided ArrayBuffer.
// This allows us to access the individual bytes stored in the ArrayBuffer.
return btoa(
// `reduce` is a method that applies a function against an accumulator and each byte in the array,
// resulting in a single output value. In this case, we're using it to build a string from the bytes.
new Uint8Array(arrayBuffer).reduce((data, byte) =>
// `String.fromCharCode(byte)` converts the byte (which is a number) to its corresponding character
// according to its Unicode value.
// Here, we concatenate the character representation of each byte to form a complete string.
data + String.fromCharCode(byte),
'')
);
}
/*
* @Description : encryptCardNoWithAES method encrypts a message using AES encryption with a base64-encoded secret.
* It generates a random initialization vector (IV), sets AES encryption options,
and performs the encryption. The method combines the IV and the encrypted message
into a single buffer and returns the base64-encoded representation of the final buffer.
* @param – msg – the plaintext message to be encrypted.
* @param – base64Secret – the base64-encoded secret key used for encryption.
* @return – string – the base64-encoded ciphertext.
*/
encryptCardNoWithAES(msg, base64Secret) {
// Generate a random initialization vector (IV) of 16 bytes for AES encryption.
// `WordArray.random(16)` creates a random byte array to ensure that the encryption is unique each time.
const iv = window.CryptoJS.lib.WordArray.random(16);
// Define the options for AES encryption.
const aesOptions = {
mode: window.CryptoJS.mode.CBC, // Use Cipher Block Chaining (CBC) mode for encryption.
padding: window.CryptoJS.pad.Pkcs7, // Use PKCS#7 padding to ensure the data is a multiple of the block size.
iv: iv // Set the initialization vector for encryption.
};
// Perform the AES encryption using the specified options.
const encryptionObj = window.CryptoJS.AES.encrypt(
msg, // The plaintext message to encrypt.
// Decode the base64-encoded secret into a format suitable for AES encryption.
window.CryptoJS.enc.Base64.parse(base64Secret),
aesOptions // Pass in the AES options defined earlier.
);
// Decode the resulting encrypted message from base64 to an ArrayBuffer.
const encryptedBuffer = this.decodeBase64ToBuffer(encryptionObj.toString());
// Decode the IV used in encryption from base64 to an ArrayBuffer.
// This is important for decryption, as the same IV must be used.
const ivBuffer = this.decodeBase64ToBuffer(encryptionObj.iv.toString(window.CryptoJS.enc.Base64));
// Combine the IV buffer and the encrypted message buffer into a single buffer.
const finalBuffer = this.combineBuffers(ivBuffer, encryptedBuffer);
// Encode the combined buffer into base64 format for easier storage or transport.
return this.encodeArrayBufferToBase64(finalBuffer);
}
/*
* @Description : handleSubmit method is invoked to send encrypted data to an Apex method.
* It retrieves the encrypted data from the component's state, calls the
sendEncryptedDataToApex` function to transmit the data, and handles
the response or error from the Apex call, logging the outcome to the console.
* @param = N.A.
* @return – N.A.
*/
handleSubmit() {
debugger;
// Retrieve the encrypted data stored in the encrypted var done in encryptCardNoWithAES.
const encryptedData = this.encrypted;
// Call the function `sendEncryptedDataToApex`, which is expected to send the encrypted data to an Apex method.
// The encrypted data is passed as an object with a property `encryptedData`.
sendEncryptedDataToApex({ encryptedData })
.then(response => {
this.displayToast('Success!', 'Data sent to Apex successfully', 'success', 'dismissable');
})
.catch(error => {
this.displayToast('Success!', 'Error sending data to Apex'+JSON.stringify(error), 'error', 'dismissable');
});
}
/*
* @Description : The displayToast method is used to display a toast notification in the Lightning Web Component.
* It creates a toast event with a specified title, message, variant, and mode, then dispatches the event to show
the notification to the user.
* @param {String} title – The title of the toast notification.
* @param {String} message – The message displayed in the toast notification.
* @param {String} variant – The type of the toast (e.g., "success", "error", "warning", "info").
* @param {String} mode – The display mode of the toast (e.g., "dismissable", "pester", "sticky").
* @return – N.A.
*/
displayToast(title, message, variant, mode) {
const evt = new ShowToastEvent({
title: title,
message: message,
variant: variant,
mode: mode
});
this.dispatchEvent(evt);
}
}

In this example:

  • CryptoJS: The library encrypts the card number in JavaScript before sending it to the server.
  • Masking: We mask the credit card number on blur, keeping only the last four digits visible to the user.
  • Sending to Apex: After encryption, we send the encrypted data to an Apex method for secure storage or further processing.

EncryptionCtrl

/*
* @Description : The EncryptionCtrl class is a public Apex class that enforces sharing rules,
* meaning it respects the user's sharing settings when accessing data.
* It contains methods for retrieving and decrypting an AES key and handling
* encrypted card numbers. The MY_KEY constant holds a sample encryption key,
* which can be accessed in test classes due to the @TestVisible annotation.
*/
public with sharing class EncryptionCtrl {
// Define a private static constant string MY_KEY, which holds a sample encryption key.
// The @TestVisible annotation allows this key to be accessed in test classes.
@TestVisible
//Added just for example here, this should be store ideally with Salesforce Shield (Encryption) or Custom Metadata as encrypted
private static final string MY_KEY = '1111111111111111';
/*
* @Description : getAESKey method retrieves the AES encryption key, encodes it as a Base64
* string for safe transmission, and logs the encoded value for debugging purposes.
* It is annotated with @AuraEnabled and cacheable=true for use in Lightning components.
* @param = N.A.
* @return – String – the Base64 encoded AES encryption key.
*/
@AuraEnabled(cacheable=true)
public static String getAESKey() {
// Encode MY_KEY as a Base64 string to safely transmit it over the network.
String base64Secret = EncodingUtil.base64Encode(Blob.valueOf(MY_KEY));
// Log the Base64 encoded secret for debugging purposes.
System.debug(base64Secret);
// Return the Base64 encoded secret.
return base64Secret;
}
/*
* @Description : getDecryptedCardNumber method decrypts a given encrypted card number using
* AES encryption with the MY_KEY as the decryption key. It logs the decrypted
* card number for debugging purposes and returns it as a string.
* @param – String encryptedData – the Base64 encoded encrypted card number to be decrypted.
* @return – String – the decrypted card number.
*/
public static String getDecryptedCardNumber(String encryptedData) {
// Decode the Base64 encoded encrypted data back into a Blob.
Blob encryptedValue = EncodingUtil.base64Decode(encryptedData);
System.debug('encryptedData+++' + encryptedData);
System.debug('encryptedValue+++' + encryptedValue);
// Decrypt the encrypted value using AES with a managed initialization vector (IV).
Blob decrypted = Crypto.decryptWithManagedIV('AES128', Blob.valueOf(MY_KEY), encryptedValue);
// Log the decrypted card number for debugging purposes.
System.debug('decrypted card number+++' + decrypted.toString());
// Return the decrypted card number as a string.
return decrypted.toString();
}
/*
* @Description : sendEncryptedData method is called from Lightning components to process
* the provided encrypted data by calling the getDecryptedCardNumber method.
* @param – String encryptedData – the Base64 encoded encrypted card number to be processed.
* @return – N.A.
*/
@AuraEnabled
public static void sendEncryptedData(String encryptedData) {
// Call getDecryptedCardNumber method to process the encrypted data.
getDecryptedCardNumber(encryptedData);
}
}

Note: Avoid decrypting data in Apex unless absolutely necessary. If possible, keep it encrypted until it reaches a secure third-party provider.

Security Tips for Client-Side Encryption in Salesforce

  1. Secure Key Management: Do not hardcode sensitive keys directly in JavaScript. Instead, use Salesforce Shield Platform Encryption or other secure methods for key management.
  2. Limit Client-Side Encryption: Whenever possible, perform encryption on the server side to improve security.
  3. End-to-End Encryption: Ensure that data remains encrypted throughout its journey to a third-party provider, especially for sensitive information.

Leave a comment