Secure Transfer (Legacy)
Learn about best practices for performing a secure transfer in the Brand Messenger Legacy
Secure Transfer is a proprietary solution within Khoros Care that gives brands the ability to bring conversations originating from third-party social networks to a secure messaging channel without losing critical contextual information from one channel to the next.
For brands, it means being able to address customer issues without putting sensitive customer information at unnecessary risk. It means building a contextual relationship with customers that links their social media profiles with your authenticated customer record. Furthermore, it enables you to more efficiently handle incoming customer requests from any channel.
For a brand's customers, it means peace of mind knowing that they can have their inquiries answered and issues resolved in a secure online environment. It demonstrates the brand's dedication to the security of its customers' information and creates a consistent customer service experience regardless of how the customer initially reaches out.
In this guide, we will cover how to set up and configure the Secure Transfer process for your brand. We will also share best practices and tips to help you get the most out of Secure Transfer and to use it with other Khoros Care features to maximize efficiency and achieve an industry-leading customer service experience.
How Secure Transfer Works
The Secure Transfer process provides a mechanism for a smooth transition of a conversation from a third-party channel to a secure channel that is under the brand's control.

Here is an example of how the user/Agent flow works:
- The end-user initiates a conversation from a third-party channel (e.g., Facebook).
- The Agent presses the Invite to Secure Messaging button in the Response form to generate a message and unique URL for the end-user.
- The Agent sends the message and the unique URL is added to the invitation and delivered to the end-user.
- The end-user is directed to a login page through the brand's identity provider (IDP) to identify the brand account the user is associated with. (Optional)
- The end-user is sent to a page that includes a secure messaging interface powered by Khoros Brand Messenger.
- The Agent is alerted that a secure chat has been initiated, and a link between the two conversations is automatically available.
- The Agent can then continue the conversation in Brand Messenger.
The portal can be built, hosted, and maintained by Khoros via a Services contract, or built and delivered by the brand’s IT department.
In this guide, we will cover each step of the Secure Transfer process, how to create and configure your Secure Chat page, and how to maximize customer success through best practices.
Step One: Send the Transfer Link
When a conversation is ready to be transferred to a secure chat, the Agent initiates the transfer by sending a shortened transfer link to the end-user. This is easily accomplished within the Khoros Care interface. Here's how:

Within the conversation panel, select the Invite to Secure Messaging button to generate a short message inviting the end-user to a secured chat experience. The message uses the same language as the conversation.

The link itself does not appear in the conversation panel. Instead, it is appended to the end of the message as it is being sent, preventing any accidental following of the link by the agent. Only the end-user can see and follow the link.

The link sends the customer to the verification portal. The URL to this portal must be provided by the brand if the brand is configuring Secure Transfer.
The link includes an encrypted payload that will contain information such as the channel the conversation was initiated through, the Agent ID associated with the conversation, etc. See the Payload section for more information about the data passed along through the generated link.
The encrypted payload is appended to the link to the portal where the secure conversation will take place. Here is a short example:
https://[Link-to-Portal]?payload=QReebTFORdgX..&iv=kAO...
Step Two: Decrypt Payload
Each unique Secure Transfer link includes an encrypted payload that passes along information about the conversation ahead of its initialization through the secure chat. This contextual information helps brands link various social media accounts and other third-party channel information to their primary customer record.
An example of payload decryption is outlined in the Decryption Java Package section.
The decryption of the payload requires two components:
- Shared key
- Initialization Vector (IV)
Once decrypted, the information looks like this:
{
"brandProviderTypeName":"twitter",
"agentId":123,
"brandProviderInstance":"twitter.com",
"authorUUID":"b95.....",
"authorProviderInstance":"twitter.com", "authorAvatarUrl":"http://abs.twimg.com/sticky/default_profile_images/default_profile_normal.png",
"imageUpload":false,
"fileUpload":false,
"authorProviderTypeName":"twitter",
"authorScreenName":"cab123",
"introMessageText":"abc",
"createdTimeMillis":1587157642353,
"conversationUUID":"d08...",
"authorName":"Chris",
"authorExternalId":"81...",
"conversationDisplayId":123,
"shareLocation":false,
"brandAuthorExternalId":"856622981484879872",
"agentUUID":"7f265fe0-186b-4ffd-b6bf-1f2e6643c017"
}
The payload includes these fields:
- brandProviderTypeName: social channel brand is responding in
- agentId: ID identifying what agent sent the invite
- brandProviderInstance: user’s social channel instance
- authorUUID: ID uniquely identifying user in Khoros Care
- authorProviderInstance: The brand’s social channel instance
- authorAvatarUrl: user’s avatar in the social channel
- imageUpload: whether image upload should be allowed on the Khoros messenger. This field can be ignored.
- fileUpload: whether file upload should be allowed on the Khoros messenger. This field can be ignored.
- authorProviderTypeName: Channel the end-user was responding from
- authorScreenName: User’s screen name in the social channel
- introMessageText: This field can be ignored
- createdTimeMillis: Time when link was sent
- conversationUUID: ID unique identifying conversation in Khoros Care
- authorName: The user’s name in Khoros Care
- authorExternalId: The user’s ID in the channel
- conversationDisplayId: The conversation ID. This is the one you want -- not conversationUUID
- shareLocation: Whether share location should be allowed on the Khoros messenger. This field can be ignored.
- brandAuthorExternalId: The brand’s ID in the social channel
- agentUUID: ID identifying what agent sent the invite
Step Three: Redirect to Login Page
At this point, the customer has been directed to the secure chat URL, the payload has been decrypted, and we're ready to identify the end-user on the brand's side.
Let's say the end-user is a customer of an e-commerce site checking on the status of their order. The brand would need to have that customer log in to verify their identity before sharing any sensitive account information.
Before the secure chat initiates, the end-user would be directed to the brand's identity provider.
Once the customer's identity is confirmed, they can then be directed to the messaging page to continue the conversation in a secure, authenticated state.
If you do not wish to require users to log in, allowing unauthenticated or anonymous flow, you can redirect to the messaging page. Khoros can help enable a pre-chat form. The form can collect any fields that you're interested in (e.g.,
Step Four: Load the Messaging Widget
To load the messaging widget, you need to load Khoros provided JavaScript on the page, and inject a handful of fields. These fields include:
Field | Source | Description |
---|---|---|
givenName | Identity Provider (IDP) | This will be the name that will display to the agent in the author profile. |
authorAvatarUrl | Payload | URL pointing to the user's avatar image. This will be displayed to the agent. |
conversationDisplayId | Payload | This is very important. This is the ID of the conversation. When this is passed, Khoros Care will automatically link the initial conversation with the new messaging conversation. It will also link the third-party channel profile with the Brand Messenger Legacy profile. |
conversationAssignedAgentId | Payload | The ID of the Agent that sent the link. This is important for the conversation linking and for analytics. |
authorScreenName | Payload | The screen name of the user on the channel. This is important for the profile linking and for analytics. |
All of the values of the fields, with the exception of givenName
, are provided by the payload. givenName
is passed along by your identity provider.
Here's an example of the full script:
<script>
window.KHOROS_CONFIG = {
companyKey: 'abc,
appId: '123',
widgetId: '123',
jwt: 'abcd',
userId: '123'
};
window.addEventListener('khorosInit', function(event) {
event.detail.sdk.updateUser({
givenName: '',
properties: {
authorAvatarUrl: 'avatar url',
conversationDisplayId: 'conversationDisplayId',
conversationAssignedAgentId: 'agentId',
authorScreenName: 'authorScreenName'
}
});
});
(function(){var scriptTag = document.createElement('script');
scriptTag.setAttribute('src','https://brand-messenger.app.khoros.com/bundle/loader.js?v=' + new Date().getTime());
document.head.appendChild(scriptTag);})();
</script>
Generate the JWT
In order for the user to be authenticated against the widget, the brand must generate a JWT token with the user's unique userId and pass it to the messaging widget.
This enables Agents to verify who the user is before discussing potentially sensitive information or performing actions with the user's account.
Khoros recommends that the userId be sourced from the identity provider (IDP). That way, the user's conversation history carries over from device to device.
Here is an example of the code that generates the token. To get the KEY_ID
and SECRET
, contact Khoros support.
The
KEY_ID
andSECRET
should never be revealed to the user.
var jwt = require('jsonwebtoken');
var KEY_ID = 'app_5deaa3example0010cc4ba4';
var SECRET = 'BFJJ88naxexampleKpBNTR';
var signJwt = function(userId) {
return jwt.sign(
{
scope: 'appUser',
userId: userId
},
SECRET,
{
header: {
alg: 'HS256',
typ: 'JWT',
kid: KEY_ID
}
}
);
};
There is an advanced use case where the verification portal redirects the user to a deep link within a mobile app (and prompts the installation of it). If Brand Messenger Legacy is already being used in the mobile app, Khoros would like to discuss this option with you.
Step Five: Agent Joins Secure Conversation
Once the user joins the secure chat session, a new Brand Messenger Legacy conversation is created and automatically linked to the original conversation from the original source channel.

The Agent can move on or pick up the conversation by selecting the link that appears in the original conversation from Agent View.

Once the conversation is picked up, the Agent will see a link leading back to the original source channel conversation, as well as to see that both profiles are now linked in the system.
Payload Decryption Java Package
In this section, we look at the Java package used to decrypt the payload sent through the link used to initiate a secure chat with the end-user. We will also examine an example of this package in use.
Decrypting Payload
The following code initiates the decryption of the payload using the files in the Java package outlined afterward.
public Optional<JSONObject> decryptPayload(String ivKey, String encryptedPayload) {
if (ivKey.isEmpty() || encryptedPayload.isEmpty()) {
return Optional.empty();
}
try {
logger.debug(String.format("Starting decryption with IV %s, PAYLOAD %s", ivKey, encryptedPayload));
final EncryptionProvider encryptionProvider = new SymmetricKeyEncryptionProvider(config.getEncryptionKey(),
ivKey,
true);
final String plainTextPayload = encryptionProvider.decryptString(encryptedPayload);
return Optional.of(new JSONObject(plainTextPayload));
} catch (EncryptionException e) {
logger.error("Issue decrypting payload: " + encryptedPayload, e);
} catch (JSONException e) {
logger.error("error trying to parse JSON payload", e);
}
return Optional.empty();
}
EncryptionProvider.java
This is the main interface.
import com.lithium.bandolier.encryption.errors.EncryptionException;
public interface EncryptionProvider {
/**
* Provide a copy of this encryption provider. (These are not thread safe!!!)
*/
EncryptionProvider copy() throws EncryptionException;
/**
* Encrypts the given plain text using the secret key and iv that was already set.
*
* MAY NOT BE THREAD SAFE. Use .clone() to get a copy and synchronize on it!
* @param plainText text to get encrypted
* @return encrypted string
*/
String encryptString(String plainText) throws EncryptionException;
/**
* Encrypts the given plain text using the secret key that was already set.
*
* MAY NOT BE THREAD SAFE. Use .clone() to get a copy and synchronize on it!
* @param plainText text to get encrypted
* @param ivKey the random iv byte[16] that has been base64 encoded
* @return encrypted string
*/
String encryptString(String plainText, String ivKey) throws EncryptionException;
/**
* Decrypts the text using the secret key and iv already set in this class.
*
* MAY NOT BE THREAD SAFE. Use .clone() to get a copy and synchronize on it!
* @param cipherText encrypted text to get decrypted
* @return decrypted string
*/
String decryptString(String cipherText) throws EncryptionException;
/**
* Decrypts the text using the secret key and iv already set in this class.
*
* MAY NOT BE THREAD SAFE. Use .clone() to get a copy and synchronize on it!
* @param cipherText encrypted text to get decrypted
* @param ivKey the random iv byte[16] that has been base64 encoded
* @return decrypted string
*/
String decryptString(String cipherText, String ivKey) throws EncryptionException;
}
SymmetricKeyEncryptionProvider.java
This is the class that implements the interface.
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.lithium.bandolier.encryption.errors.EncryptionException;
import org.apache.commons.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.concurrent.NotThreadSafe;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Optional;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Implementation of encryption using symmetric encryption with secret key and initialization vector.
*
* !!! WARNING !!! This class is NOT thread-safe. To be more specific, this class can be shared across multiple threads
* so long as one thread is doing ONLY encryption and the other thread is doing ONLY decryption. The Cipher object is
* what is not thread safe.
*/
@NotThreadSafe
public class SymmetricKeyEncryptionProvider implements EncryptionProvider {
private static final Logger logger = LoggerFactory.getLogger(SymmetricKeyEncryptionProvider.class);
private static final String AES_CBC_PKCS5_PADDING = "AES/CBC/PKCS5Padding";
static final boolean URL_SAFE_ENCODING = true;
static final byte[] CHUNK_SEPARATOR = {'\r', '\n'};
private final Cipher aesEncryptCipher; // Not ThreadSafe
private final Cipher aesDecryptCipher; // Not ThreadSafe
private final SecretKey secretKey;
private final String ivKey;
private final boolean shouldBase64DecodeKeys;
public SymmetricKeyEncryptionProvider(String secretKey, String ivKey) throws EncryptionException {
this(secretKey, ivKey, false); // for backwards compatibility
}
protected SymmetricKeyEncryptionProvider(final SecretKey secretKey, final String ivKey, final boolean shouldBase64DecodeKeys) throws EncryptionException {
try {
aesEncryptCipher = Cipher.getInstance(AES_CBC_PKCS5_PADDING);
aesDecryptCipher = Cipher.getInstance(AES_CBC_PKCS5_PADDING);
} catch (NoSuchAlgorithmException | NoSuchPaddingException ex) {
logger.error("Cannot initialize cipher.", ex);
throw new EncryptionException("Cannot initialize cipher.", ex);
}
this.secretKey = secretKey;
this.ivKey = ivKey;
this.shouldBase64DecodeKeys = shouldBase64DecodeKeys;
}
public SymmetricKeyEncryptionProvider(String secretKey, String ivKey, boolean shouldBase64DecodeKeys) throws EncryptionException {
try {
// The AES encryption in CBC mode is considered one of the most secure and fast symmetrical encryption schemes adopted by the US government and many other organizations worldwide.
// Using default JCE provider that comes with standard JDKs to avoid problems with packaging encryption jars inside shaded mailman jar.
aesEncryptCipher = Cipher.getInstance(AES_CBC_PKCS5_PADDING);
aesDecryptCipher = Cipher.getInstance(AES_CBC_PKCS5_PADDING);
checkNotNull(secretKey);
// ----------------------------------------------------------------------
// secret keys and IVs do not go into URLs and do not need to use Base64 URL-safe encoding
//
this.secretKey = new SecretKeySpec(shouldBase64DecodeKeys ? Base64.decodeBase64(secretKey.getBytes()) : secretKey.getBytes(), "AES");
this.ivKey = checkNotNull(ivKey);
this.shouldBase64DecodeKeys = shouldBase64DecodeKeys;
} catch (NoSuchAlgorithmException | NoSuchPaddingException ex) {
logger.error("Cannot initialize cipher.", ex);
throw new EncryptionException("Cannot initialize cipher.", ex);
}
}
@Override
public EncryptionProvider copy() throws EncryptionException {
return new SymmetricKeyEncryptionProvider(new SecretKeySpec(this.secretKey.getEncoded(), "AES"), ivKey, shouldBase64DecodeKeys);
}
@Override
public String encryptString(String plainText) throws EncryptionException {
return encryptString(plainText, ivKey);
}
@Override
public String encryptString(String plainText, String ivKey) throws EncryptionException {
try {
aesEncryptCipher.init(Cipher.ENCRYPT_MODE, secretKey, getIVSpec(ivKey));
byte[] encryptedBytes = aesEncryptCipher.doFinal(plainText.getBytes(Charsets.UTF_8));
// ----------------------------------------------------------------------
// we use the static method instead of the instance supplied by this.createBase64()
// because the static method has extra error handling already built in. There is
// a dependency between the args used here and the args used in the constructor inside
// this.createBase64().
//
byte[] encodedBytes = Base64.encodeBase64(encryptedBytes, false, URL_SAFE_ENCODING);
return encodedBytes == null ? null : new String(encodedBytes, Charsets.UTF_8);
} catch (InvalidAlgorithmParameterException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException ex) {
logger.warn("Cannot encrypt string.", ex);
throw new EncryptionException("Cannot encrypt string.", ex);
}
}
@Override
public String decryptString(String cipherText) throws EncryptionException {
return decryptString(cipherText, ivKey);
}
@Override
public String decryptString(String cipherText, String ivKey) throws EncryptionException {
try {
aesDecryptCipher.init(Cipher.DECRYPT_MODE, secretKey, getIVSpec(ivKey));
// ----------------------------------------------------------------------
// The built in static methods do not have a form that allows URL safe decoding, so
// we construct the right type of Base64 and then decode directly with it.
//
final Base64 b64 = createBase64();
byte[] decodedCipherText = b64.decode(cipherText.getBytes(Charsets.UTF_8));
return new String(aesDecryptCipher.doFinal(decodedCipherText), Charsets.UTF_8);
} catch (IllegalBlockSizeException | InvalidKeyException | InvalidAlgorithmParameterException | BadPaddingException ex) {
logger.info("Cannot decrypt string");
throw new EncryptionException("Cannot decrypt string.", ex);
}
}
/**
* Create a new initialization-vector to facilitate rotating IV's in application code for each piece of data.
* @return a SecureRandom initialized byte[16] that has been base64 encoded.
*/
public static String generateNewIV() {
byte[] iv = new byte[16];
new SecureRandom().nextBytes(iv);
// ----------------------------------------------------------------------
// secret keys and IVs were NOT encoded with URL_SAFE base64 originally, and there is no point
// and much risk in changing them. So, we leave their codec unchanged.
//
return new String(Base64.encodeBase64(iv, false), Charsets.UTF_8);
}
public static Optional<String> generateNewKey() {
try {
final int keyLength = 256;
final String algorithm = "AES";
final KeyGenerator keyGen = KeyGenerator.getInstance(algorithm);
keyGen.init(keyLength);
final SecretKey secretKey = keyGen.generateKey();
return Optional.of(Base64.encodeBase64String(secretKey.getEncoded()));
} catch (NoSuchAlgorithmException e) {
logger.error("Unable to generate an encryption key", e);
return Optional.empty();
}
}
@VisibleForTesting
Base64 createBase64() {
return new Base64(0 /* means no chunking */, CHUNK_SEPARATOR, URL_SAFE_ENCODING);
}
private IvParameterSpec getIVSpec(String ivKey) {
// ----------------------------------------------------------------------
// secret keys and IVs were NOT encoded with URL_SAFE base64 originally, and there is no point
// and much risk in changing them. So, we leave their codec unchanged.
//
return new IvParameterSpec(shouldBase64DecodeKeys ? Base64.decodeBase64(ivKey.getBytes()) : ivKey.getBytes());
}
}
Cryptor.java
import com.google.common.io.BaseEncoding;
import javax.annotation.Nullable;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import static java.nio.charset.StandardCharsets.UTF_8;
public class Cryptor {
public static final String AES_GCM = "AES/GCM/NoPadding";
public static final String AES_CBC = "AES/CBC/PKCS5PADDING";
private final SecretKeySpec secretKey;
private final SecureRandom secureRandom;
public Cryptor(String encryptionKey) {
try {
this.secretKey = new SecretKeySpec(aesHashBlock(encryptionKey), "AES");
this.secureRandom = new SecureRandom();
} catch (NoSuchAlgorithmException nsae) {
throw new RuntimeException(nsae);
}
}
// encryptionKey byte array must be a valid AES key size.
public Cryptor(byte[] encryptionKey) {
this.secretKey = new SecretKeySpec(encryptionKey, "AES");
this.secureRandom = new SecureRandom();
}
/**
* Securely generate N bytes of random data and return as base64 encoded string.
* @return base64 encoded string
*/
public String randomBase64(int size) {
byte[] bytes = new byte[size];
secureRandom.nextBytes(bytes);
return BaseEncoding.base64().encode(bytes);
}
/**
* Generate a 128 bit hash block from a provided string for use in AES calculations.
* @param s performs a SHA-256 hash and returns first 16 bytes.
* @return byte array of hash
* @throws NoSuchAlgorithmException
*/
public byte[] aesHashBlock(String s) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(s.getBytes(UTF_8));
// Return only 16 bytes of the hash
return Arrays.copyOf(md.digest(), 16);
}
public byte[] decryptCBC(byte[] value, byte[] iv)
throws BadPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException,
InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException {
Cipher cipher = Cipher.getInstance(AES_CBC);
cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));
return cipher.doFinal(value);
}
public byte[] decryptGCM(byte[] value, byte[] iv, @Nullable byte[] aad)
throws BadPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException,
InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException {
Cipher cipher = Cipher.getInstance(AES_GCM);
cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(128, iv));
if (aad != null) {
cipher.updateAAD(aad);
}
return cipher.doFinal(value);
}
public byte[] encryptCBC(byte[] value, byte[] iv)
throws BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException,
InvalidKeyException, NoSuchPaddingException, NoSuchAlgorithmException {
Cipher cipher = Cipher.getInstance(AES_CBC);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));
return cipher.doFinal(value);
}
public byte[] encryptGCM(byte[] value, byte[] iv, @Nullable byte[] aad)
throws BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException,
InvalidKeyException, NoSuchPaddingException, NoSuchAlgorithmException {
Cipher cipher = Cipher.getInstance(AES_GCM);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, new GCMParameterSpec(128, iv));
if (aad != null) {
cipher.updateAAD(aad);
}
return cipher.doFinal(value);
}
}
EncryptionException.java
This class serves the simple purpose of initiating an exception message should something go awry during the encryption/decryption process.
public class EncryptionException extends Exception {
public EncryptionException(String message, Throwable ex) {
super(message, ex);
}
}
Updated 6 months ago