When developing integration between Salesforce and other application we sometimes face issue regarding storing sensitive information like password and security token.
Salesforce encrypted fields wasn't able to provide good enough solution, as users with permission View Encrypted Data can view it, in addition during summer 17' upgrade Salesforce update the encryption field functionality and those values are no longer masked or encrypted when viewed in Salesforce.
However, Salesforce does provide Ecnrypto class that provide some method for encrypt/decrypt. Still, you might face new challenge when you need to use the encryped data in other application (in my case Java).
In this example I demonstrate how we can use such encryption process. It contains the following steps:
1. Page for entering encrypted data.
2. Encryption setup in Salesforce.
3. Utility process that get text and encrypt it based on the setup from 1.
4. Trigger for the specific object that encrypt the data
5. Process for replacing the encrypted data.
6. Java util class that decode the data.
Lets start working:
1. Page for entering encrypted data
This is not required, but without this step, when user will enter his security information it will be visible during typing. It's preferred to mask it, and it isn't complex to solved it.
The component apex:inputSecret does the masking for us.
Assume we have custom object User_Credential__c, with the fields User_Name__c, Password__c, Security_Token__c, then we can create use the following visualforce page.
<apex:page standardController="User_Credential__c" tabStyle="User_Credential__c">
<apex:form >
<apex:sectionHeader title="Enter Credentials" subtitle="Enter Credentials"/>
<apex:pageBlock>
<apex:pageBlockButtons >
<apex:commandButton action="{!save}" value="Save"/>
<apex:commandButton action="{!cancel}" value="Cancel"/>
</apex:pageBlockButtons>
<apex:pageBlockSection columns="2">
<apex:inputField value="{!User_Credential__c.Name}"/>
<apex:inputField value="{!User_Credential__c.User_Name__c}"/>
<apex:inputSecret value="{!User_Credential__c.Password__c}"/>
<apex:inputSecret value="{!User_Credential__c.Security_Token__c}"/>
</apex:pageBlockSection>
</apex:pageBlock>
</apex:form>
</apex:page>
2. Encryption setup in Salesforce
We don't want to use hard coded values. Therefore we need to setup few settings + place holder for our encryption key. I use custom setting. Named it "Encryption Settings", and added 3 fields there:
After the setup create record in this custom settings. The Encryption Method that we will use is
AES128, the Encryption Self Len we will use is
10. This is just dummy text that will added to the text before encryption in order to defend against hacking.
How to get your Encryption Key? You can either wait to step 4, where we will add step that generate such key, or use script that generate key and store it in the custom settings:
Blob key = Crypto.generateAesKey(128);
String keyString = EncodingUtil.base64Encode(key);
System.debug('Encryption Key: ' + keyString);
3. Utility process that get text and encrypt it based on the setup from 1
We need function that receive input String and encode it based on the custom setting. Below simply code does the job. Most of logic is based on standard functions from classes
EncodingUtil and
Crypto.
public class EncryptionUtilities{
public static String hashString(String input){
String encryptKey = Encryption_Settings__c.getInstance().Encryption_Key__c;
String encryptMethod = Encryption_Settings__c.getInstance().Encryption_Method__c;
Integer saltLen = Integer.valueOf(Encryption_Settings__c.getInstance().Encryption_Salt_Len__c);
String salt = EncodingUtil.convertToHex(Crypto.generateAesKey(128)).substring(0, saltLen);
Blob encrypted = Crypto.encryptWithManagedIV(encryptMethod, EncodingUtil.base64Decode(encryptKey), Blob.valueOf(input + salt));
return EncodingUtil.base64Encode(encrypted);
}
}
4.Trigger for the specific object that encrypt the data
In the custom object User_Credential__c we will setup trigger that when new record created or the password/token were changed it should encrypt those values.
The trigger will use the function from step 2
trigger UserCredential_Trigger on User_Credential__c (before insert, before update) {
if(Trigger.isInsert || Trigger.isUpdate){
for(User_Credential__c userCredential: Trigger.new){
if((userCredential.Password__c != null)
&& (Trigger.isInsert || (Trigger.isUpdate && userCredential.Password__c != Trigger.oldMap.get(userCredential.Id).Password__c))){
userCredential.Password__c = EncryptionUtilities.hashString(userCredential.Password__c);
}
if((userCredential.Security_Token__c != null)
&& (Trigger.isInsert || (Trigger.isUpdate && userCredential.Security_Token__c != Trigger.oldMap.get(userCredential.Id).Security_Token__c))){
userCredential.Security_Token__c = EncryptionUtilities.hashString(userCredential.Security_Token__c);
}
}
}
}
5. Process for replacing the encrypted data
It might be good idea to provide process for replacing the encryption key, for cases it was stolen. In such process, we need to change the Encryption Key in the custom setting and replace all existing values that encrypted with the old key, as we won't be able to decode them with the new key. Also note that from the code, you can learn how to decode the encryption data in using Salesforce standard Encrypto class.
This function can be added to the class that used in step 2 - EncryptionUtilities - and you might want to provide page/button that invoke it.
//Used to replace the Encryption Key in custom setting, and recreate
//all the encryptions in existing records
public static String replaceEncriptionKey(){
String retMsg = '';
Savepoint sp = Database.setSavepoint();
try{
//Get current setting from custom metadata
Encryption_Settings__c encryptionSettings = [SELECT Id, Encryption_Key__c FROM Encryption_Settings__c LIMIT 1];
String currentEncriptionKey = encryptionSettings.Encryption_Key__c;
//Generate new encryption key
Blob key = Crypto.generateAesKey(128);
String keyString = EncodingUtil.base64Encode(key);
//save new encryption key to custom metadata - use apex Metadata API
encryptionSettings.Encryption_Key__c = keyString;
update encryptionSettings;
//Replace existing encrypted values
String encryptMethod = Encryption_Settings__c.getInstance().Encryption_Method__c;
Integer saltLen = Integer.valueOf(Encryption_Settings__c.getInstance().Encryption_Salt_Len__c);
List<User_Credential__c> l_userCredentials = new List<User_Credential__c>();
for(User_Credential__c userCredential : [SELECT Id, Password__c, Security_Token__c FROM User_Credential__c]){
Blob blobAfter64DecodePass = EncodingUtil.base64Decode(userCredential.Password__c);
Blob blobAfterDecodePass = Crypto.decryptWithManagedIV(encryptMethod, EncodingUtil.base64Decode(currentEncriptionKey), blobAfter64DecodePass);
String originalPass= blobAfterDecodePass.toString();
userCredential.Password__c = originalPass.substring(0, originalPass.length() - saltLen);
Blob blobAfter64DecodeToken = EncodingUtil.base64Decode(userCredential.Security_Token__c );
Blob blobAfterDecodeToken = Crypto.decryptWithManagedIV(encryptMethod, EncodingUtil.base64Decode(currentEncriptionKey), blobAfter64DecodeToken);
String originalToken = blobAfterDecodeToken.toString();
userCredential.Security_Token__c = originalToken.substring(0, originalToken.length() - saltLen);
l_userCredentials.add(userCredential);
}
//save all records - this should invoke the trigger that will use the new custom setting
update l_userCredentials;
}
catch(Exception ex){
Database.rollback(sp);
retMsg = 'Excpetion : ' + ex.getMessage();
}
return retMsg;
}
6. Java util class that decode the data.
The main method - getDescryptedAsString - get String encrypted and the key for decrypt and return the original text.
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.util.Arrays;
import java.util.Base64;
public class ExternalDecoder {
private static final String characterEncoding = "UTF-8";
private static final String cipherTransformation = "AES/CBC/PKCS5Padding";
private static final String aesEncryptionAlgorithm = "AES";
public static String getDecryptedAsString(String encryptedText, String key) throws Exception{
return new String(decryptBase64EncodedWithManagedIV(encryptedText, key), characterEncoding);
}
public static byte[] decryptBase64EncodedWithManagedIV(String encryptedText, String key) throws Exception {
byte[] cipherText = Base64.getDecoder().decode(encryptedText.getBytes());
byte[] keyBytes = Base64.getDecoder().decode(key.getBytes());
return decryptWithManagedIV(cipherText, keyBytes);
}
public static byte[] decryptWithManagedIV(byte[] cipherText, byte[] key) throws Exception{
byte[] initialVector = Arrays.copyOfRange(cipherText,0,16);
byte[] trimmedCipherText = Arrays.copyOfRange(cipherText,16,cipherText.length);
return decrypt(trimmedCipherText, key, initialVector);
}
public static byte[] decrypt(byte[] cipherText, byte[] key, byte[] initialVector) throws Exception{
Cipher cipher = Cipher.getInstance(cipherTransformation);
SecretKeySpec secretKeySpecy = new SecretKeySpec(key, aesEncryptionAlgorithm);
IvParameterSpec ivParameterSpec = new IvParameterSpec(initialVector);
cipher.init(Cipher.DECRYPT_MODE, secretKeySpecy, ivParameterSpec);
cipherText = cipher.doFinal(cipherText);
return cipherText;
}
}
Usage of the code:
String encryptPassword = <get value from SF>;
String encryptionKey =<get value from SF or store it in some setup file>;
String decodeValue = ExternalDecoder.getDecryptedAsString(encryptPassword, encryptionKey);
String password = decodeValue.substring(0, decodeValue.length()-10);