diff --git a/lib/faraday/uid2.rb b/lib/faraday/uid2.rb new file mode 100644 index 0000000..46d5422 --- /dev/null +++ b/lib/faraday/uid2.rb @@ -0,0 +1,8 @@ +require_relative "uid2/middleware" +require "faraday" + +module Faraday + module Uid2 + Faraday::Request.register_middleware uid2_encryption: Middleware + end +end diff --git a/lib/faraday/uid2/middleware.rb b/lib/faraday/uid2/middleware.rb new file mode 100644 index 0000000..2a78008 --- /dev/null +++ b/lib/faraday/uid2/middleware.rb @@ -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 diff --git a/lib/uid2.rb b/lib/uid2.rb index 335ca83..ca98094 100644 --- a/lib/uid2.rb +++ b/lib/uid2.rb @@ -1,4 +1,6 @@ # frozen_string_literal: true require_relative "uid2/version" + +require_relative "faraday/uid2" require_relative "uid2/client" diff --git a/lib/uid2/client.rb b/lib/uid2/client.rb index f277ef9..ae41b93 100644 --- a/lib/uid2/client.rb +++ b/lib/uid2/client.rb @@ -3,6 +3,7 @@ require "net/http/persistent" require "faraday" require "faraday_middleware" +require "faraday/uid2" require "time" module Uid2 @@ -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) @@ -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) @@ -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? @@ -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