Cryptographic Failures is the second most important category of vulnerabilities listed in the OWASP Top 10 for 2021. In this blog post, we’ll cover techniques to encrypt and hash data in Apex when that data needs to be transmitted to or from an external system. We’ll share code examples and we’ll explain when to choose each technique.
OWASP #2: Cryptographic Failures
OWASP (Open Web Application Security Project) is a nonprofit foundation that works to improve software security. Recently, OWASP published its Top 10 list for 2021. One of the categories that has moved up on the list is Cryptographic Failures, which is now in the second position (A02). This means that today this problem is even more critical and frequent than it was in 2017 (when the category was named “Sensitive Data Exposure“).
This category covers failures related to cryptography, including the lack of cryptographic mechanisms, which often leads to exposure of sensitive data. This is especially important for integrations, where data is transmitted from one system to another. Luckily, the Salesforce Platform and Apex have cryptographic utilities and algorithms in place to ensure that data is securely transmitted without compromise.
What data needs to be encrypted?
The first question to answer here is: what kind of data should be encrypted, and how? Passwords, credit card numbers, health records, personal information, and business secrets are some examples of sensitive data. We should encrypt such data, not only for our own security, but also (in some cases) to comply with regulations like the GDPR or the PCI data security standard. It is recommended to encrypt data at rest (on disk) and when it’s being transmitted through the network (in transit) — even if we are using secure protocols like HTTPS. In this blog post, we’ll focus on encrypting and hashing data in transit. If you want to know more about encryption at rest, take a look at our Shield Platform Encryption feature.
Information security key concepts
Different algorithms are available to encrypt data in transit, and each can enforce one or more of the following characteristics of the communication process:
- Confidentiality (secrecy): ensures that a message is transmitted in a format (normally encrypted), so that unauthorized users are not able disclose what the message contains.
- Integrity: ensures that the message has not been altered during the transmission (not tampered with). Confidentiality and integrity are independent. For instance, a a message can be transmitted in clear text, not being secret, while still preserving its integrity.
- Authenticity: ensures that the message was sent by the sender who claims it. Algorithms that ensure authenticity often imply integrity as well.
- Non-repudiation: is a stronger concept than authenticity. It adds legal proof that ensures that the sender sent the message.
The Apex Crypto class contains pre-built functions to help you implement secure encryption algorithms. Let’s take a look at some of them.
AES encryption
The Crypto.encrypt()
method allows you to encrypt data before it is sent to a receiver using the AES algorithm. This ensures confidentiality. The AES algorithm is a block cipher (operates with blocks of a fixed size) algorithm that takes plain text in blocks of 128 bits and converts them to cipher text. The mode of operation for AES in Apex is CBC mode. The cipher text follows the PKCS7 padding syntax. Equivalently, the Crypto.decrypt()
method allows you to decrypt data that’s been received in cypher text.
You can choose between AES128, AES192, and AES256 for the algorithm. Crypto classes often offer multiple algorithm versions, and when possible, you should choose the highest bit option common to Salesforce and the third-party system. Another factor to consider is compute time: the more complex the algorithm, the more time it will take to encrypt and decrypt.
AES uses a symmetric key of 128, 192, or 256 bits (the later being the most secure option). The chosen key length does not need to match the chosen algorithm version. There’s a Crypto.generateAESKey()
method that you can use to generate the key.
A symmetric key is a shared secret that both the sender and receiver have to encrypt and decrypt the message. Contrast this with asymmetric keys, where the sender encrypts using the receiver’s public key, and the receiver decrypts using his/her own private key. There’s one particular situation — digital signatures — where the sender uses their private key to encrypt (see the section on this below).
Symmetric keys need to be shared in a secure way. The recommendation is to share them offline or send them encrypted, typically using PKI. Also, remember that keys should be stored in a safe way in Salesforce. In managed packages, use a protected custom metadata record or a protected custom setting for that purpose.
The AES algorithm also needs an initialization vector (IV). The size of the IV is based on the algorithm mode. For CBC, it’s 128 bits. This is a random number used to ensure that the same value encrypted multiple times doesn’t always result in the same encrypted value.
The following diagram shows where the IV is used in CBC mode. Note how the output would be the same for a given input and key if there was no IV.
To decrypt, you not only need the symmetric key, but also the IV. You can generate a custom IV for encrypting the data, and send it together with the ciphertext for decryption on the receiver. Bear in mind that the IV needs to be different on each operation.
In most cases, it’s easier to use the Crypto.encryptWithManagedIV()
method that generates a random initialization vector for you, and transmits it in the first 128 bits (16 bytes) of the encrypted Blob. In that case, you decrypt the data with Crypto.decryptWithManagedIV()
. We strongly recommend that you follow this approach.
Here you have some sample Apex code that shows how to use these methods:
/**
* @description Encrypts data using AES algorithm, which needs a symmetric key to be shared with the receiver.
* In this case the initialization vector is managed by Salesforce.
* @param dataToEncrypt Blob that contains the data to encrypt
* @return Blob
* @example
* Blob dataToEncrypt = Blob.valueOf('Test data');
* Blob encryptedData = EncryptionRecipes.encryptAES256WithManagedIVRecipe(dataToEncrypt);
* System.debug(EncodingUtil.base64Encode(encryptedData));
**/
@AuraEnabled
public static Blob encryptAES256WithManagedIVRecipe(Blob dataToEncrypt) {
// Call Crypto.encryptWithManagedIV specifying the selected AES Algorithm
return Crypto.encryptWithManagedIV(
AESAlgorithm.AES256.name(),
AES_KEY,
dataToEncrypt
);
}
/**
* @description Encrypts data using AES algorithm, which needs a symmetric key to be shared with the receiver.
* In this case the initialization vector will be the first 128 bits (16 bytes) of the received data.
* @param dataToDecrypt Blob that contains the data to be decrypted
* @return Blob
* @example
* Blob decryptedData = EncryptionRecipes.decryptAES256WithManagedIVRecipe(encryptedData);
* System.debug(decryptedData.toString());
**/
@AuraEnabled
public static Blob decryptAES256WithManagedIVRecipe(Blob dataToDecrypt) {
// Call Crypto.decryptWithManagedIV specifying the selected AES Algorithm
return Crypto.decryptWithManagedIV(
AESAlgorithm.AES256.name(),
AES_KEY,
dataToDecrypt
);
}
Note that in all the examples, we’ve encoded Blobs (variables that hold binary data) in Base64 (binary to text encoding), so that you can see a String when testing them, but data can be transmitted in Blob format.
Although we’ve written both pieces of code in Apex, the most common use case will be to send the data to an external system or vice-versa. Typically, other programming languages have similar libraries to implement the exact same behavior. For example, take a look at this C# or these JavaScript solutions. Note that when encrypting with Crypto.encryptWithManagedIV()
, the external system will have to obtain the IV from the first 128 bits (16 bytes) of the received ciphertext for decryption.
Hash digest
The Crypto.generateDigest()
method generates a one-way hash digest using MD5, SHA1, SHA256, or SHA512 (the later being the most secure one). The hash digest process applies an algorithm to the input, resulting in a final string of a fixed length. Hash functions are one-way by design. It is infeasible to reverse a cryptographic hash. While you cannot reverse a hash, the algorithms are designed to be compatible, allowing you to hash the same information on multiple systems. Each system, using the same algorithm, generates an identical hash. Then, the receiver will be sure that the message has not been tampered with — the integrity of the message has been preserved. Notice that generating and checking a hash digest doesn’t ensure confidentiality. If you need the message to be confidential, you’ll have to additionally use an encryption algorithm. Also, a hash by itself doesn’t ensure authenticity. We’ll cover HMAC and digital signatures, which do provide it, later in the post.
Here you have some Apex code that shows how to use these methods for an emitter and for a receiver of the message:
/**
* @description Generates one-way hash digest that can be checked in destination to ensure integrity.
* @param dataToHmac Blob that contains some data for which to generate a hash
* @return Blob
* @example
* Blob dataToHash = Blob.valueOf('Test data');
* Blob hash = EncryptionRecipes.generateSHA512HashRecipe();
* System.debug(EncodingUtil.base64Encode(hash));
**/
@AuraEnabled
public static Blob generateSHA512HashRecipe(Blob dataToHash) {
// Call Crypto.generateDigest specifying the selected algorithm
return Crypto.generateDigest(HashAlgorithm.SHA512.name(), dataToHash);
}
/**
* @description Recomputes hash digest for and compares it with the received one, throwing an exception if they're not equal.
* @param hash Blob that contains the received hash
* @param dataToCheck Blob that contains the data to check the hash for
* @return void
* @example
* try {
* EncryptionRecipes.checkSHA512HashRecipe(hash, corruptedData);
* } catch(Exception e) {
* // Should log exception
* System.debug(e.getMessage());
* }
**/
@AuraEnabled
public static void checkSHA512HashRecipe(Blob hash, Blob dataToCheck) {
Blob recomputedHash = Crypto.generateDigest(
HashAlgorithm.SHA512.name(),
dataToCheck
);
// recomputedHash and hash should be identical!
if (!areEqualConstantTime(hash, recomputedHash)) {
throw new CryptographicException('Wrong hash!');
}
}
Note that areEqualConstantTime()
is a method that compares two Blobs in constant time in order to avoid timing attack effects.
/**
* Comparisons which involve cryptography need to be performed in constant time
* using specialized functions to avoid timing attack effects.
* https://en.wikipedia.org/wiki/Timing_attack
* @param first first String to compare
* @param second second String to compare
* @return Boolean strings are equal
*/
public static boolean areEqualConstantTime(String first, String second) {
Boolean result = true;
if (first.length() != second.length()) {
result = false;
}
Integer max = first.length() > second.length()
? second.length()
: first.length();
for (Integer i = 0; i < max; i++) {
if (first.substring(i, i + 1) != second.substring(i, i + 1)) {
result = false;
}
}
return result;
}
HMAC
The Crypto.generateMac()
method (see reference) generates a Hash-Based Message Authentication Code (HMAC). With HMAC, a secret key (symmetric) is used to derive two keys that are used in the hashing process. This way, not only is integrity assured, but also authenticity as we can ensure that the MAC for the message was generated using the secret key. The supported HMAC algorithms are HMACMD5, HMACSHA1, HMACSHA256, and HMACSHA512 (the later being the most secure).
The only restriction for the key is that it is less than 4KB. It’s recommended to use different keys for the encryption of the message and for the HMAC.
Here you have some Apex code to generate a HMAC in the sender and check for its validity on the receiver.
/**
* @description Generates one-way HMAC (using a symmetric key) that can be checked in destination to ensure integrity and authenticity.
* @param dataToHmac Blob that contains some data for which to generate an HMAC
* @return Blob
* @example
* Blob dataToHmac = Blob.valueOf('Test data');
* Blob hmac = EncryptionRecipes.generateHMACSHA512Recipe();
* System.debug(EncodingUtil.base64Encode(hmac));
**/
@AuraEnabled
public static Blob generateHMACSHA512Recipe(Blob dataToHmac) {
// Call Crypto.generateMac specifying the selected algorithm
return Crypto.generateMac(
HMACAlgorithm.HMACSHA512.name(),
dataToHmac,
HMAC_KEY
);
}
/**
* @description Recomputes HMAC using the symmetric key and compares it with the received one, throwing an exception if they're not equal.
* @param hmac Blob that contains the received hmac
* @param dataToCheck Blob that contains the data to check the hmac for
* @return void
* @example
* try {
* EncryptionRecipes.checkHMACSHA512Recipe(hmac, corruptedData);
* } catch(Exception e) {
* // Should log exception
* System.debug(e.getMessage());
* }
**/
@AuraEnabled
public static void checkHMACSHA512Recipe(Blob hmac, Blob dataToCheck) {
Boolean correct = Crypto.verifyHMAC(
HMACAlgorithm.HMACSHA512.name(),
dataToCheck,
HMAC_KEY,
hmac
);
if (!correct) {
throw new CryptographicException('Wrong HMAC!');
}
}
Digital signature
Although symmetric keys should be shared in a safe way, as they are shared, there’s always a possibility that somebody else may have used them in order to pretend to be the sender. That’s why we use digital signatures to ensure non-repudiation. Digital signatures use an asymmetric key in which the private part of the key is never shared. So, when a message is digitally signed, it’s considered legally proven that the sender sent the message.
With the Crypto.sign()
method, you can compute a unique signature for the message using the specified signing algorithm and the supplied private key — in this case, the sender’s portion of an asymmetrical key. The valid algorithms are RSA, RSA-SHA1, RSA-SHA256, RSA-SHA384, RSA-SHA512, ECDSA-SHA256, ECDSA-SHA384, and ECDSA-SHA512. There are several factors to take into account when comparing RSA and ECDSHA algorithms, so evaluate them in depth before choosing. In this case, integrity, authenticity, and non-repudiation of the message are enforced.
The asymmetric key must be in RSA’s PKCS #8 syntax. Check how to generate a key pair with openssl.
Here you have some code examples to better understand how to use the methods:
/**
* @description Generates one-way Digital Signature (encrypted with an asymmetric key) that can be checked in destination to ensure integrity, authenticity and non-repudiation.
* @param dataToSign Blob that contains some data to sign
* @return Blob
* @example
* Blob dataToSign = Blob.valueOf('Test data');
* Blob signature = EncryptionRecipes.generateRSASHA512DigitalSignatureRecipe();
* System.debug(EncodingUtil.base64Encode(signature));
**/
@AuraEnabled
public static Blob generateRSASHA512DigitalSignatureRecipe(
Blob dataToSign
) {
// Call Crypto.sign specifying the selected algorithm
return Crypto.sign(
DigitalSignatureAlgorithm.RSA_SHA512.name().replace('_', '-'),
dataToSign,
DIGITAL_SIGNATURE_PRIVATE_KEY
);
}
/**
* @description Recomputes Digital Signature for and compares it with the received one, throwing an exception if they're not equal.
* @param signature Blob that contains the received signature
* @param dataToCheck Blob that contains the data to check the signature for
* @return void
* @example
* try {
* EncryptionRecipes.checkRSASHA512DigitalSignatureRecipe(signature, corruptedData);
* } catch(Exception e) {
* // Should log exception
* System.debug(e.getMessage());
* }
**/
@AuraEnabled
public static void checkRSASHA512DigitalSignatureRecipe(
Blob signature,
Blob dataToCheck
) {
Boolean correct = Crypto.verify(
DigitalSignatureAlgorithm.RSA_SHA512.name().replace('_', '-'),
dataToCheck,
signature,
DIGITAL_SIGNATURE_PUBLIC_KEY
);
if (!correct) {
throw new CryptographicException('Wrong signature!');
}
}
Alternatively, you can sign a message using Crypto.signWithCertificate()
, which takes the name of a X509 certificate that contains a private key, and then calls Crypto.sign()
internally. Use Crypto.signXML()
to sign an XML document.
Signing a document digitally is the most robust approach, but bear in mind that the greater the complexity, the longer will take the algorithm to execute. The execution time will be an important factor to take into account in the algorithm selection.
Combining encryption and signature algorithms
Combining encryption and signature algorithms will provide any combination of confidentiality, integrity, and non-repudiation features that you need. For instance, take a look at this example in which we combine encryption with a digital signature algorithm:
public class EncryptedAndSignedData {
public Blob encryptedData;
public Blob signature;
}
/**
* @description Encrypts the message with AES and then generates Digital Signature (encrypted with an asymmetric key) that can be checked in destination.
* This ensure confidentiality, integrity, authenticity and non-repudiation.
* @param dataToEncryptAndSign Blob that contains some data to encrypt and sign
* @return Blob
* @example
* Blob dataToEncryptAndSign = Blob.valueOf('Test data');
* EncryptedAndSignedData wrapper = EncryptionRecipes.encryptAES256AndGenerateRSASHA512DigitalSignRecipe();
* System.debug(EncodingUtil.base64Encode(wrapper.encryptedData));
* System.debug(EncodingUtil.base64Encode(wrapper.signature));
**/
@AuraEnabled
public static EncryptedAndSignedData encryptAES256AndGenerateRSASHA512DigitalSignRecipe(
Blob dataToEncryptAndSign
) {
// Call Crypto.encrypt specifying the selected algorithm
Blob encryptedData = Crypto.encryptWithManagedIV(
AESAlgorithm.AES256.name(),
AES_KEY,
dataToEncryptAndSign
);
// Call Crypto.sign specifying the selected algorithm
Blob signature = Crypto.sign(
DigitalSignatureAlgorithm.RSA_SHA512.name().replace('_', '-'),
encryptedData,
DIGITAL_SIGNATURE_PRIVATE_KEY
);
EncryptedAndSignedData wrapper = new EncryptedAndSignedData();
wrapper.encryptedData = encryptedData;
wrapper.signature = signature;
return wrapper;
}
/**
* @description Decrypts the message and verifies its Digital Signature.
* @param signature Blob that contains the received signature
* @param dataToDecryptAndCheck Blob that contains the data to check the signature for
* @return Blob decrypted data
* @example
* try {
* EncryptionRecipes.decryptAES256AndCheckRSASHA512DigitalSignRecipe(signature, corruptedData);
* } catch(Exception e) {
* // Should log exception
* System.debug(e.getMessage());
* }
**/
@AuraEnabled
public static Blob decryptAES256AndCheckRSASHA512DigitalSignRecipe(
Blob signature,
Blob dataToDecryptAndCheck
) {
Boolean correct = Crypto.verify(
DigitalSignatureAlgorithm.RSA_SHA512.name().replace('_', '-'),
dataToDecryptAndCheck,
signature,
DIGITAL_SIGNATURE_PUBLIC_KEY
);
if (!correct) {
throw new CryptographicException('Wrong signature!');
}
return Crypto.decryptWithManagedIV(
AESAlgorithm.AES256.name(),
AES_KEY,
dataToDecryptAndCheck
);
}
Summary
In this blog post, we’ve covered different encryption and signature techniques that you can use to avoid cryptographic failures when transmitting data. Each of the techniques ensures certain aspects of the communication process that you can find summarized in this table:
Also, consider the algorithm complexity versus the time it takes to execute when choosing the right algorithm for your use case.
We’ve added all the code examples highlighted in this blog post to Apex Recipes. Also, learn more about encryption in the Trailhead module: Use Encryption in Custom Applications.
About the author
Alba Rivas works as a Principal Developer Advocate at Salesforce. She focuses on Lightning Web Components and Lightning adoption strategy. You can follow her on Twitter @AlbaSFDC.