Skip to content

Instantly share code, notes, and snippets.

@thewheat
Last active March 21, 2021 19:05
Show Gist options
  • Save thewheat/47ee52af3e496f26b07db96ae965ce94 to your computer and use it in GitHub Desktop.
Save thewheat/47ee52af3e496f26b07db96ae965ce94 to your computer and use it in GitHub Desktop.
Sample code to enable Intercom Encrypted Mode

Intercom Encrypted Mode

How it looks like

Before

window.intercomSettings = {
  app_id: "a1b2c3d4",
  email: “[email protected]”,
  favourite_color: “blue”,
  pricing_plan: “gold”
};

After

window.intercomSettings = {
  app_id: "a1b2c3d4",
};
window.intercomEncryptedPayload = ‘PcWsDRQCM/xB5gyhaBBuas81891+ahdanbLLlkhOoRsrShn76n7nnn089jfjS8R5yrIRD8N9Hnn4g89dqH+1izo’;

Sample implementations

  • These are listed below and each language / platform has its own file with some documentation within that file itself.
  • Feel free to contribute others that are not listed 😀
  • Below is some sample data that could help with any implementations

Sample data for implementation

Starting from a text identity verification value of mysecret

$secret = "mysecret"; // $key = hex2bin(hash("sha256", $secret));

It should yield a key value

base64_encode($key) == "ZSx9xofZjJiJME7S5AjHS2EehqQMqlHEtD8d1ZE8XNA="

For the encryption portion it will take 3 inputs $plaintext, $key, $iv and return a $ciphertext and $tag

$ciphertext = doEncyption($plaintext, $key, $iv, $tag);

Using the following plaintext

$plaintext = "{\"user_id\":\"1\",\"user_hash\":\"20dd4a92e1bcda4403a0de48ef5e5d4b0544a7f2317270e58519af9bc29010be\"}"

With this initislisation vector (in real world example this should be randomised)

base64_encode($iv) == "q83vASNFZ4k=" // $iv = hex2bin("abcdef0123456789");

It should return $ciphertext and $tag of the following values

base64_encode($ciphertext) == "iv8ExqxYd4n9/PN4hIDfTMgWFzi/Z5ox+TehpT1sU2qR9RwRRJqEgHZy/ks4zFORDoPnBPYlDwR7Sm8/9U+7rSSzkTxunKWRUMjNcKdOAYaxJLZOMCpxycctbEnWsA=="
base64_encode($tag) == "tfzI3dkc+NYTMttuGCrszQ=="

The final output intercomEncryptedPayload value should be as follows

intercomEncryptedPayload == "q83vASNFZ4mK/wTGrFh3if3883iEgN9MyBYXOL9nmjH5N6GlPWxTapH1HBFEmoSAdnL+SzjMU5EOg+cE9iUPBHtKbz/1T7utJLORPG6cpZFQyM1wp04BhrEktk4wKnHJxy1sSdawtfzI3dkc+NYTMttuGCrszQ=="
// Node: Express + Jade
///////////////////////////////////////////////////////////////////////////////
// index.js
var express = require('express');
var router = express.Router();
var crypto = require('crypto');
// Settings
const intercomSettings = {app_id: "YOUR_APP_ID", user_id: "1"};
const secret = "YOUR_WORKSPACE_IDENTITY_VERIFICATION_FOR_WEB_SECRET"; // https://app.intercom.io/a/apps/_/settings/identity-verification/web
// Demo implementation of using `aes-256-gcm` with node.js's `crypto` lib.
// https://gist.github.com/rjz/15baffeab434b8125ca4d783f4116d81
const aes256gcm = (key) => {
const ALGO = 'aes-256-gcm';
// encrypt returns base64-encoded ciphertext
const encrypt = (str) => {
// Hint: the `iv` should be unique (but not necessarily random).
// `randomBytes` here are (relatively) slow but convenient for
// demonstration.
const iv = new Buffer(crypto.randomBytes(12), 'utf8'); // Uses 12 https://github.com/intercom/intercom-rails/blob/1478cd606478a0254f7efc2a5960cf5db44e9ee3/lib/intercom-rails/encrypted_mode.rb#L9 which seems to be what is typically used https://crypto.stackexchange.com/questions/41601/aes-gcm-recommended-iv-size-why-12-bytes/41610
const cipher = crypto.createCipheriv(ALGO, key, iv);
// Hint: Larger inputs (it's GCM, after all!) should use the stream API
let enc = cipher.update(str, 'utf8', 'base64');
enc += cipher.final('base64');
return [enc, iv, cipher.getAuthTag()];
};
// decrypt decodes base64-encoded ciphertext into a utf8-encoded string
const decrypt = (enc, iv, authTag) => {
const decipher = crypto.createDecipheriv(ALGO, key, iv);
decipher.setAuthTag(authTag);
let str = decipher.update(enc, 'base64', 'utf8');
str += decipher.final('utf8');
return str;
};
return {
encrypt,
decrypt,
};
};
function getEncryptionModeSettings(settings,get_encryptable_data=true){
const ENCRYPTED_MODE_SETTINGS_WHITELIST = ["app_id", "session_duration", "widget", "custom_launcher_selector", "hide_default_launcher", "alignment", "horizontal_padding", "vertical_padding"];
const output_settings = {};
Object.keys(settings).forEach(function (key) {
if (get_encryptable_data && ENCRYPTED_MODE_SETTINGS_WHITELIST.includes(key)) return;
if (!get_encryptable_data && !ENCRYPTED_MODE_SETTINGS_WHITELIST.includes(key)) return;
output_settings[key] = settings[key];
});
return output_settings;
}
function encryptData(encryptSettings, secret){
const KEY = crypto.createHash('sha256')
.update(secret)
.digest();
const aesCipher = aes256gcm(KEY);
const [encrypted, iv, authTag] = aesCipher.encrypt(JSON.stringify(encryptSettings));
return iv.toString('base64') + encrypted + authTag.toString('base64');
}
/* GET home page. */
router.get('/', function(req, res, next) {
const intercomSettingsOutput = JSON.stringify(getEncryptionModeSettings(intercomSettings,false));
const encryptSettings = getEncryptionModeSettings(intercomSettings);
const encryptedOutput = encryptData(encryptSettings, secret);
res.render('index', { title: 'Express', app_id: intercomSettings.app_id, intercomSettings: intercomSettings, intercomSettingsOutput: intercomSettingsOutput, encryptedOutput: encryptedOutput});
});
module.exports = router;
///////////////////////////////////////////////////////////////////////////////
// layout.jade
doctype html
html
head
title= title
link(rel='stylesheet', href='/stylesheets/style.css')
body
block content
script APP_ID = "#{app_id}"
script (function(){var w=window;var ic=w.Intercom;if(typeof ic==="function"){ic('reattach_activator');ic('update',w.intercomSettings);}else{var d=document;var i=function(){i.c(arguments);};i.q=[];i.c=function(args){i.q.push(args);};w.Intercom=i;var l=function(){var s=d.createElement('script');s.type='text/javascript';s.async=true;s.src='https://widget.intercom.io/widget/' + APP_ID;var x=d.getElementsByTagName('script')[0];x.parentNode.insertBefore(s,x);};if(w.attachEvent){w.attachEvent('onload',l);}else{w.addEventListener('load',l,false);}}})();
script window.intercomSettings= !{intercomSettingsOutput}
script window.intercomEncryptedPayload= "!{encryptedOutput}"
<?php
$app_id = "YOUR_APP_ID";
$secret = "YOUR_WORKSPACE_IDENTITY_VERIFICATION_FOR_WEB_SECRET"; // https://app.intercom.io/a/apps/_/settings/identity-verification/web
$user_id = "1";
$intercomSettings = [
"app_id" => $app_id,
"custom_launcher_selector" => ".abc",
"user_id" => $user_id,
"user_hash" => hash_hmac('sha256', $user_id, $secret)
];
// based on logic defined here
// https://github.com/intercom/intercom-rails/blob/1478cd606478a0254f7efc2a5960cf5db44e9ee3/lib/intercom-rails/encrypted_mode.rb#L21-L32
const ENCRYPTED_MODE_SETTINGS_WHITELIST = ["app_id", "session_duration", "widget", "custom_launcher_selector", "hide_default_launcher", "alignment", "horizontal_padding", "vertical_padding"];
function encrypt($plaintext, $key_string)
{
$cipher = "aes-256-gcm";
$key = hex2bin(hash("sha256", $key_string));
if (in_array($cipher, openssl_get_cipher_methods()))
{
$ivlen = openssl_cipher_iv_length($cipher);
$iv = openssl_random_pseudo_bytes($ivlen);
$ciphertext = openssl_encrypt($plaintext , $cipher, $key, OPENSSL_RAW_DATA, $iv, $tag);
$output = $iv . $ciphertext . $tag;
return base64_encode($output);
}
}
function getEncryptableSettings($settings,$encryptable=true)
{
$output_settings = [];
foreach($settings as $key => $value)
{
if ($encryptable && in_array($key, ENCRYPTED_MODE_SETTINGS_WHITELIST)) continue;
if (!$encryptable && !in_array($key, ENCRYPTED_MODE_SETTINGS_WHITELIST)) continue;
$output_settings[$key] = $value;
}
return $output_settings;
}
$intercomSettingsOutput = json_encode(getEncryptableSettings($intercomSettings, false));
$ciphertext = encrypt(json_encode(getEncryptableSettings($intercomSettings)), $secret);
?>
<html>
<head>
<title>Intercom Encrypted Mode in PHP</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style type="text/css">
pre {
white-space: pre-wrap; /* Since CSS 2.1 */
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
white-space: -pre-wrap; /* Opera 4-6 */
white-space: -o-pre-wrap; /* Opera 7 */
word-wrap: break-word; /* Internet Explorer 5.5+ */
}
</style>
<script>
APP_ID = "<?php echo $app_id ?>";
(function(){var w=window;var ic=w.Intercom;if(typeof ic==="function"){ic('reattach_activator');ic('update',intercomSettings);}else{var d=document;var i=function(){i.c(arguments)};i.q=[];i.c=function(args){i.q.push(args)};w.Intercom=i;function l(){var s=d.createElement('script');s.type='text/javascript';s.async=true;s.src='https://widget.intercom.io/widget/'+APP_ID;var x=d.getElementsByTagName('script')[0];x.parentNode.insertBefore(s,x);}if(w.attachEvent){w.attachEvent('onload',l);}else{w.addEventListener('load',l,false);}}})()
</script>
</head>
<body>
<h1>Intercom Encrypted Mode in PHP</h1>
<ul>
<li><a href="https://developerblog.intercom.com/hide-sensitive-user-information-with-encrypted-mode-791ce3478241">Blog post on Encrypted Mode</a></li>
<li>Created based on <a href="https://github.com/intercom/intercom-rails/blob/1478cd606478a0254f7efc2a5960cf5db44e9ee3/lib/intercom-rails/encrypted_mode.rb#L21-L32">logic from Rails Gem that has Encrypted Mode</a></li>
</ul>
<h3>App ID <?php echo $app_id; ?></h3>
<hr>
<h2>Normal non encrypted mode</h2>
<pre>window.intercomSettings = <?php echo json_encode($intercomSettings) ?></pre>
<hr>
<h2>Encrypted mode</h2>
<pre>window.intercomSettings = <?php echo $intercomSettingsOutput; ?>;</pre>
<pre>window.intercomEncryptedPayload = "<?php echo $ciphertext; ?>";</pre>
<script type="text/javascript">
window.intercomSettings = <?php echo $intercomSettingsOutput; ?>;
window.intercomEncryptedPayload = "<?php echo $ciphertext; ?>";
</script>
</body>
</html>
@MylesWardell
Copy link

MylesWardell commented Mar 10, 2020

This code is a little outdated for nodeJS, after some experimentation this is what I have found works:

Our Encryption Package

const Crypto = {} 
Crypto['aes-256-gcm'] = {
  encrypt: ({ key, data, iv, ivLength = 16 }) => {
    iv = iv ? Buffer.from(iv) : Buffer.from(crypto.randomBytes(ivLength))
    const cipher = crypto.createCipheriv('aes-256-gcm', key, iv)
    let encrypted = cipher.update(data, 'utf8', 'base64')
    encrypted += cipher.final('base64')
    return { encrypted, iv, authTag: cipher.getAuthTag() }
  },

  decrypt: ({ key, iv, data, authTag }) => {
    const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv)
    if (authTag) decipher.setAuthTag(authTag)
    let decrypted = decipher.update(data, 'base64', 'utf8')
    decrypted += decipher.final('utf8')
    return { decrypted }
  }
}

Crypto.createHash = ({ type = 'sha256', key }) => {
  return crypto.createHash(type)
    .update(key)
    .digest()
}

Encrypting Intercom Settings

const settings = JSON.stringify({
      user_id,
      user_hash,
      email,
      name
    })
const key = Crypto.createHash({ key: INTERCOM_SECRET })
const { encrypted, iv, authTag } = Crypto['aes-256-gcm'].encrypt({ key, data: settings, ivLength: 12 })

// This is inside a function
return Buffer.concat([iv, Buffer.from(encrypted, 'base64'), authTag]).toString('base64')

Frontend

window.intercomEncryptedPayload = intercomEncryptedPayload
Intercom('boot', {
    app_id: APP_ID
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment