Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Encrypted payload support #263

Merged
merged 1 commit into from
Jun 15, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/intercom-rails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require 'intercom-rails/proxy'
require 'intercom-rails/proxy/user'
require 'intercom-rails/proxy/company'
require 'intercom-rails/encrypted_mode'
require 'intercom-rails/script_tag'
require 'intercom-rails/script_tag_helper'
require 'intercom-rails/custom_data_helper'
Expand Down
1 change: 1 addition & 0 deletions lib/intercom-rails/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ def self.reset!
config_accessor :enabled_environments, &ARRAY_VALIDATOR
config_accessor :include_for_logged_out_users
config_accessor :hide_default_launcher
config_accessor :encrypted_mode

def self.api_key=(*)
warn "Setting an Intercom API key is no longer supported; remove the `config.api_key = ...` line from config/initializers/intercom.rb"
Expand Down
34 changes: 34 additions & 0 deletions lib/intercom-rails/encrypted_mode.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
module IntercomRails
class EncryptedMode
attr_reader :secret, :initialization_vector, :enabled

ENCRYPTED_MODE_SETTINGS_WHITELIST = [:app_id, :session_duration, :widget, :custom_launcher_selector, :hide_default_launcher, :alignment, :horizontal_padding, :vertical_padding]

def initialize(secret, initialization_vector, options)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we just encapsulate the IV generation (through securerandom) and drop it from the list of the function args?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It helps to able to Dependency Inject it so that we can specify fixed IVs for things like testing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@secret = secret
@initialization_vector = initialization_vector || SecureRandom.random_bytes(12)
@enabled = options.fetch(:enabled, false)
end

def plaintext_part(settings)
enabled ? settings.slice(*ENCRYPTED_MODE_SETTINGS_WHITELIST) : settings
end

def encrypted_javascript(payload)
enabled ? "window.intercomEncryptedPayload = \"#{encrypt(payload)}\";" : ""
end

def encrypt(payload)
return nil unless enabled
payload = payload.except(*ENCRYPTED_MODE_SETTINGS_WHITELIST)
key = Digest::SHA256.digest(secret)
cipher = OpenSSL::Cipher.new('aes-256-gcm')
cipher.encrypt
cipher.key = key
cipher.iv = initialization_vector
json = ActiveSupport::JSON.encode(payload).gsub('<', '\u003C')
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty sure that this gsub is not necessary as the results are encrypted and Base64'd anyway. However retaining it for now.

encrypted = initialization_vector + cipher.update(json) + cipher.final + cipher.auth_tag
Base64.encode64(encrypted).gsub("\n", "\\n")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just so that the result can be interpolated as JavaScript without a syntax error caused by line breaks.

end
end
end
19 changes: 16 additions & 3 deletions lib/intercom-rails/script_tag.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class ScriptTag
include ::ActionView::Helpers::JavaScriptHelper

attr_reader :user_details, :company_details, :show_everywhere, :session_duration
attr_accessor :secret, :widget_options, :controller, :nonce
attr_accessor :secret, :widget_options, :controller, :nonce, :encrypted_mode_enabled, :encrypted_mode

def initialize(options = {})
self.secret = options[:secret] || Config.api_secret
Expand All @@ -20,6 +20,9 @@ def initialize(options = {})
@session_duration = session_duration_from_config
self.user_details = options[:find_current_user_details] ? find_current_user_details : options[:user_details]

self.encrypted_mode_enabled = options[:encrypted_mode] || Config.encrypted_mode
self.encrypted_mode = IntercomRails::EncryptedMode.new(secret, options[:initialization_vector], {:enabled => encrypted_mode_enabled})

# Request specific custom data for non-signed up users base on lead_attributes
self.user_details = self.user_details.merge(find_lead_attributes)

Expand Down Expand Up @@ -94,11 +97,21 @@ def find_lead_attributes
custom_data.select {|k, v| lead_attributes.map(&:to_s).include?(k)}
end

def plaintext_settings
encrypted_mode.plaintext_part(intercom_settings)
end

def encrypted_settings
encrypted_mode.encrypt(intercom_settings)
end

private

def intercom_javascript
intercom_settings_json = ActiveSupport::JSON.encode(intercom_settings).gsub('<', '\u003C')
plaintext_javascript = ActiveSupport::JSON.encode(plaintext_settings).gsub('<', '\u003C')
intercom_encrypted_payload_javascript = encrypted_mode.encrypted_javascript(intercom_settings)

str = "window.intercomSettings = #{intercom_settings_json};(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='#{Config.library_url || "https://widget.intercom.io/widget/#{j 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);}};})()"
str = "window.intercomSettings = #{plaintext_javascript};#{intercom_encrypted_payload_javascript}(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='#{Config.library_url || "https://widget.intercom.io/widget/#{j 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);}};})()"

str
end
Expand Down
5 changes: 5 additions & 0 deletions spec/config_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@
expect(IntercomRails.config.hide_default_launcher).to eq(true)
end

it 'gets/sets Encrypted Mode' do
IntercomRails.config.encrypted_mode = true
expect(IntercomRails.config.encrypted_mode).to eq(true)
end

it 'raises error if current user not a proc' do
expect { IntercomRails.config.user.current = 1 }.to raise_error(ArgumentError)
end
Expand Down
27 changes: 27 additions & 0 deletions spec/encrypted_mode_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
require 'spec_helper'

describe IntercomRails::EncryptedMode do
it 'whitelists certain attributes' do
encrypted_mode = IntercomRails::EncryptedMode.new("foo", nil, {:enabled => true})
expect(encrypted_mode.plaintext_part({:app_id => "bar", :baz => "bang"})).to eq({:app_id => "bar"})
end

it "encrypts correctly" do
encrypted_mode = IntercomRails::EncryptedMode.new("foo", "a"*12, {:enabled => true})
encrypted = encrypted_mode.encrypt({"baz" => "bang"})

decoded = Base64.decode64(encrypted)

cipher = OpenSSL::Cipher.new('aes-256-gcm')
cipher.decrypt
cipher.key = Digest::SHA256.digest("foo")
cipher.iv = decoded[0, 12]
auth_tag_index = decoded.length - 16
cipher.auth_tag = decoded[auth_tag_index, 16]
ciphertext = decoded[12, decoded.length - 16 - 12]
result = cipher.update(ciphertext) + cipher.final

original = JSON.parse(result)
expect(original).to eq({"baz" => "bang"})
end
end
22 changes: 22 additions & 0 deletions spec/script_tag_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,28 @@
IntercomRails.config.app_id = before
end

context 'Encrypted Mode' do
it 'sets an encrypted payload' do
iv = Base64.decode64("2X0G4PoOBn9+wdf8")
script_tag = ScriptTag.new(:user_details => {:email => 'ciaran@intercom.io'}, :secret => 'abcdefgh', :encrypted_mode => true, :initialization_vector => iv)
result = script_tag.to_s
expect(result).to_not include("ciaran@intercom.io")
expect(result).to match(/window\.intercomEncryptedPayload = \"[^\"\n]+\"/)
end

it "#plaintext_settings" do
script_tag = ScriptTag.new(:user_details => {:email => 'ciaran@intercom.io'}, :secret => 'abcdefgh', :encrypted_mode => true)
expect(script_tag.plaintext_settings).to_not include(:email)
script_tag = ScriptTag.new(:user_details => {:email => 'ciaran@intercom.io'}, :secret => 'abcdefgh', :encrypted_mode => false)
expect(script_tag.plaintext_settings).to include(:email)
end

it "#encrypted_settings" do
script_tag = ScriptTag.new(:user_details => {:email => 'ciaran@intercom.io'}, :secret => 'abcdefgh', :encrypted_mode => true)
expect(script_tag.encrypted_settings).to match(/[^\"\n]+/)
end
end

context 'Identity Verification - user_hash' do

it 'computes user_hash using email when email present, and user_id blank' do
Expand Down