Skip to content
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
8 changes: 8 additions & 0 deletions lib/faraday/uid2.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
require_relative "uid2/middleware"
require "faraday"

module Faraday
module Uid2
Faraday::Request.register_middleware uid2_encryption: Middleware
end
end
76 changes: 76 additions & 0 deletions lib/faraday/uid2/middleware.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# frozen_string_literal: true

require "faraday"
require "base64"
require "hashie/mash"

module Faraday
module Uid2
class Middleware < Faraday::Middleware
def initialize(app, secret_key, is_refresh, options = {})
super(app, options)

@key = Base64.decode64(secret_key)
@is_refresh = is_refresh
end

def call(request_env)
unless @is_refresh
@nonce = Random.new.bytes(8)

cipher = create_cipher.encrypt
iv = cipher.random_iv

body = request_env.body
payload = timestamp_bytes + @nonce + body
encrypted = cipher.update(payload) + cipher.final
request_env.body = Base64.strict_encode64(["\x1", iv, encrypted, cipher.auth_tag].join)
end

@app.call(request_env).on_complete do |response_env|
process_response(response_env)
end
end

def process_response(env)
resp = Base64.decode64(env.body).unpack("C*")
iv = resp[0..11].pack("C*")
cipher_text = resp[12...-16].pack("C*")
auth_tag = resp[-16...-1].pack("C*")

cipher = create_cipher.decrypt
cipher.iv = iv
cipher.auth_tag = auth_tag

payload = cipher.update(cipher_text) + cipher.final

data = if @is_refresh
payload
else
timestamp = Time.at(payload[0..7].unpack1("Q>") / 1000.0)
raise Faraday::ParsingError.new("Response timestamp is too old", env[:response]) if Time.now - timestamp > 5 * 60

nonce = payload[8..15]
raise Faraday::ParsingError.new("Nonce mismatch", env[:response]) if nonce != @nonce

payload[16..-1]
end

env.response_headers["Content-Type"] = "application/json"
env.body = Hashie::Mash.new(JSON.parse(data))
end

def timestamp_bytes
[(Time.now.to_f * 1000).to_i].pack("Q>")
end

def create_cipher
cipher = OpenSSL::Cipher.new("aes-256-gcm").encrypt
cipher.padding = 0
cipher.key = @key
cipher.auth_data = ""
cipher
end
end
end
end
2 changes: 2 additions & 0 deletions lib/uid2.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# frozen_string_literal: true

require_relative "uid2/version"

require_relative "faraday/uid2"
require_relative "uid2/client"
39 changes: 11 additions & 28 deletions lib/uid2/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require "net/http/persistent"
require "faraday"
require "faraday_middleware"
require "faraday/uid2"
require "time"

module Uid2
Expand All @@ -12,7 +13,7 @@ class Client
def initialize(_options = {})
yield(self) if block_given?

self.base_url ||= "https://integ.uidapi.com/v1/"
self.base_url ||= "https://prod.uidapi.com/v2/"
end

def generate_token(email: nil, email_hash: nil)
Expand All @@ -25,7 +26,7 @@ def generate_token(email: nil, email_hash: nil)
else
{email: email}
end
http.get("token/generate", params).body
http.post("token/generate", params).body
end

def validate_token(token:, email: nil, email_hash: nil)
Expand All @@ -37,36 +38,22 @@ def validate_token(token:, email: nil, email_hash: nil)
{email: email}
end

http.get("token/validate", params.merge(token: token)).body
http.post("token/validate", params.merge(token: token)).body
end

def refresh_token(refresh_token:)
http.get("token/refresh", {refresh_token: refresh_token}).body
def refresh_token(refresh_token:, refresh_response_key:)
http(is_refresh: true, refresh_response_key: refresh_response_key).post("token/refresh", refresh_token).body
end

def get_salt_buckets(since: Time.now)
# By default, Ruby's iso8601 generates timezone parts (`T`)
# which needs to be removed for UID2 APIs
http.get("identity/buckets", since_timestamp: since.utc.iso8601[0..-2]).body
http.post("identity/buckets", since_timestamp: since.utc.iso8601[0..-2]).body
end

def generate_identifier(email: nil, email_hash: nil)
raise ArgumentError, "Either email or email_hash needs to be provided" if email.nil? && email_hash.nil?

# As stated in doc, if email and email_hash are both supplied in the same request,
# only the email will return a mapping response.
params = if email.empty?
{email_hash: email_hash}
else
{email: email}
end

http.get("identity/map", params).body
end

def batch_generate_identifier(email: nil, email_hash: nil)
raise ArgumentError, "Either email or email_hash needs to be provided" if email.nil? && email_hash.nil?

# As stated in doc, if email and email_hash are both supplied in the same request,
# only the email will return a mapping response.
params = if email.empty?
Expand All @@ -86,17 +73,13 @@ def credentials
}
end

def http
@http ||= Faraday.new(
def http(is_refresh: false, refresh_response_key: nil)
Faraday.new(
url: base_url,
headers: credentials
) do |f|
f.request :json

f.response :raise_error
f.response :mashify
f.response :json

f.request :json unless refresh_response_key
f.request :uid2_encryption, refresh_response_key || ENV["UID2_SECRET_KEY"], is_refresh
f.adapter :net_http_persistent
end
end
Expand Down