# coding: utf-8 require 'nokogiri' module ActiveMerchant #:nodoc: module Billing #:nodoc: # = Redsys Merchant Gateway # # Gateway support for the Spanish "Redsys" payment gateway system. This is # used by many banks in Spain and is particularly well supported by # Catalunya Caixa's ecommerce department. # # Redsys requires an order_id be provided with each transaction and it must # follow a specific format. The rules are as follows: # # * First 4 digits must be numerical # * Remaining 8 digits may be alphanumeric # * Max length: 12 # # If an invalid order_id is provided, we do our best to clean it up. # # Much of the code for this library is based on the active_merchant_sermepa # integration gateway which uses essentially the same API but with the # banks own payment screen. # # Written by Samuel Lown for Cabify. For implementation questions, or # test access details please get in touch: sam@cabify.com. # # *** SHA256 Authentication Update *** # # Redsys is dropping support for the SHA1 authentication method. This # adapter has been updated to work with the new SHA256 authentication # method, however in your initialization options hash you will need to # specify the key/value :signature_algorithm => "sha256" to use the # SHA256 method. Otherwise it will default to using the SHA1. # # class RedsysGateway < Gateway self.live_url = "https://sis.sermepa.es/sis/operaciones" self.test_url = "https://sis-t.redsys.es:25443/sis/operaciones" self.supported_countries = ['ES'] self.default_currency = 'EUR' self.money_format = :cents # Not all card types may be activated by the bank! self.supported_cardtypes = [:visa, :master, :american_express, :jcb, :diners_club] self.homepage_url = "http://www.redsys.es/" self.display_name = "Redsys" CURRENCY_CODES = { "AED" => '784', "ARS" => '32', "AUD" => '36', "BRL" => '986', "BOB" => '68', "CAD" => '124', "CHF" => '756', "CLP" => '152', "CNY" => '156', "COP" => '170', "CRC" => '188', "CZK" => '203', "DKK" => '208', "DOP" => '214', "EUR" => '978', "GBP" => '826', "GTQ" => '320', "HUF" => '348', "IDR" => '360', "INR" => '356', "JPY" => '392', "KRW" => '410', "MYR" => '458', "MXN" => '484', "NOK" => '578', "NZD" => '554', "PEN" => '604', "PLN" => '616', "RUB" => '643', "SAR" => '682', "SEK" => '752', "SGD" => '702', "THB" => '764', "TWD" => '901', "USD" => '840', "UYU" => '858' } # The set of supported transactions for this gateway. # More operations are supported by the gateway itself, but # are not supported in this library. SUPPORTED_TRANSACTIONS = { :purchase => 'A', :authorize => '1', :capture => '2', :refund => '3', :cancel => '9' } # These are the text meanings sent back by the acquirer when # a card has been rejected. Syntax or general request errors # are not covered here. RESPONSE_TEXTS = { 0 => "Transaction Approved", 400 => "Cancellation Accepted", 481 => "Cancellation Accepted", 500 => "Reconciliation Accepted", 900 => "Refund / Confirmation approved", 101 => "Card expired", 102 => "Card blocked temporarily or under susciption of fraud", 104 => "Transaction not permitted", 107 => "Contact the card issuer", 109 => "Invalid identification by merchant or POS terminal", 110 => "Invalid amount", 114 => "Card cannot be used to the requested transaction", 116 => "Insufficient credit", 118 => "Non-registered card", 125 => "Card not effective", 129 => "CVV2/CVC2 Error", 167 => "Contact the card issuer: suspected fraud", 180 => "Card out of service", 181 => "Card with credit or debit restrictions", 182 => "Card with credit or debit restrictions", 184 => "Authentication error", 190 => "Refusal with no specific reason", 191 => "Expiry date incorrect", 201 => "Card expired", 202 => "Card blocked temporarily or under suspicion of fraud", 204 => "Transaction not permitted", 207 => "Contact the card issuer", 208 => "Lost or stolen card", 209 => "Lost or stolen card", 280 => "CVV2/CVC2 Error", 290 => "Declined with no specific reason", 480 => "Original transaction not located, or time-out exceeded", 501 => "Original transaction not located, or time-out exceeded", 502 => "Original transaction not located, or time-out exceeded", 503 => "Original transaction not located, or time-out exceeded", 904 => "Merchant not registered at FUC", 909 => "System error", 912 => "Issuer not available", 913 => "Duplicate transmission", 916 => "Amount too low", 928 => "Time-out exceeded", 940 => "Transaction cancelled previously", 941 => "Authorization operation already cancelled", 942 => "Original authorization declined", 943 => "Different details from origin transaction", 944 => "Session error", 945 => "Duplicate transmission", 946 => "Cancellation of transaction while in progress", 947 => "Duplicate tranmission while in progress", 949 => "POS Inoperative", 950 => "Refund not possible", 9064 => "Card number incorrect", 9078 => "No payment method available", 9093 => "Non-existent card", 9218 => "Recursive transaction in bad gateway", 9253 => "Check-digit incorrect", 9256 => "Preauth not allowed for merchant", 9257 => "Preauth not allowed for card", 9261 => "Operating limit exceeded", 9912 => "Issuer not available", 9913 => "Confirmation error", 9914 => "KO Confirmation" } # Creates a new instance # # Redsys requires a login and secret_key, and optionally also accepts a # non-default terminal. # # ==== Options # # * :login -- The Redsys Merchant ID (REQUIRED) # * :secret_key -- The Redsys Secret Key. (REQUIRED) # * :terminal -- The Redsys Terminal. Defaults to 1. (OPTIONAL) # * :test -- +true+ or +false+. Defaults to +false+. (OPTIONAL) # * :signature_algorithm -- +"sha256"+ Defaults to +"sha1"+. (OPTIONAL) def initialize(options = {}) requires!(options, :login, :secret_key) options[:terminal] ||= 1 options[:signature_algorithm] ||= "sha1" super end def purchase(money, payment, options = {}) requires!(options, :order_id) data = {} add_action(data, :purchase) add_amount(data, money, options) add_order(data, options[:order_id]) add_payment(data, payment) data[:description] = options[:description] data[:store_in_vault] = options[:store] commit data end def authorize(money, payment, options = {}) requires!(options, :order_id) data = {} add_action(data, :authorize) add_amount(data, money, options) add_order(data, options[:order_id]) add_payment(data, payment) data[:description] = options[:description] data[:store_in_vault] = options[:store] commit data end def capture(money, authorization, options = {}) data = {} add_action(data, :capture) add_amount(data, money, options) order_id, _, _ = split_authorization(authorization) add_order(data, order_id) data[:description] = options[:description] commit data end def void(authorization, options = {}) data = {} add_action(data, :cancel) order_id, amount, currency = split_authorization(authorization) add_amount(data, amount, :currency => currency) add_order(data, order_id) data[:description] = options[:description] commit data end def refund(money, authorization, options = {}) data = {} add_action(data, :refund) add_amount(data, money, options) order_id, _, _ = split_authorization(authorization) add_order(data, order_id) data[:description] = options[:description] commit data end def verify(creditcard, options = {}) MultiResponse.run(:use_first_response) do |r| r.process { authorize(100, creditcard, options) } r.process(:ignore_result) { void(r.authorization, options) } end end def supports_scrubbing true end def scrub(transcript) transcript. gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]'). gsub(%r((%3CDS_MERCHANT_PAN%3E)\d+(%3C%2FDS_MERCHANT_PAN%3E))i, '\1[FILTERED]\2'). gsub(%r((%3CDS_MERCHANT_CVV2%3E)\d+(%3C%2FDS_MERCHANT_CVV2%3E))i, '\1[FILTERED]\2'). gsub(%r(()\d+())i, '\1[FILTERED]\2'). gsub(%r(()\d+())i, '\1[FILTERED]\2'). gsub(%r((DS_MERCHANT_CVV2)%2F%3E%0A%3C%2F)i, '\1[BLANK]'). gsub(%r((DS_MERCHANT_CVV2)%2F%3E%3C)i, '\1[BLANK]'). gsub(%r((DS_MERCHANT_CVV2%3E)(%3C%2FDS_MERCHANT_CVV2))i, '\1[BLANK]\2'). gsub(%r(()())i, '\1[BLANK]\2'). gsub(%r((DS_MERCHANT_CVV2%3E)\++(%3C%2FDS_MERCHANT_CVV2))i, '\1[BLANK]\2'). gsub(%r(()\s+())i, '\1[BLANK]\2') end private def add_action(data, action) data[:action] = transaction_code(action) end def add_amount(data, money, options) data[:amount] = amount(money).to_s data[:currency] = currency_code(options[:currency] || currency(money)) end def add_order(data, order_id) data[:order_id] = clean_order_id(order_id) end def url test? ? test_url : live_url end def add_payment(data, card) if card.is_a?(String) data[:credit_card_token] = card else name = [card.first_name, card.last_name].join(' ').slice(0, 60) year = sprintf("%.4i", card.year) month = sprintf("%.2i", card.month) data[:card] = { :name => name, :pan => card.number, :date => "#{year[2..3]}#{month}", :cvv => card.verification_value } end end def commit(data) parse(ssl_post(url, "entrada=#{CGI.escape(xml_request_from(data))}", headers)) end def headers { 'Content-Type' => 'application/x-www-form-urlencoded' } end def xml_request_from(data) if sha256_authentication? build_sha256_xml_request(data) else build_sha1_xml_request(data) end end def build_signature(data) str = data[:amount] + data[:order_id].to_s + @options[:login].to_s + data[:currency] if card = data[:card] str << card[:pan] str << card[:cvv] if card[:cvv] end str << data[:action] if data[:store_in_vault] str << 'REQUIRED' elsif data[:credit_card_token] str << data[:credit_card_token] end str << @options[:secret_key] Digest::SHA1.hexdigest(str) end def build_sha256_xml_request(data) xml = Builder::XmlMarkup.new xml.instruct! xml.REQUEST do build_merchant_data(xml, data) xml.DS_SIGNATUREVERSION 'HMAC_SHA256_V1' xml.DS_SIGNATURE sign_request(merchant_data_xml(data), data[:order_id]) end xml.target! end def build_sha1_xml_request(data) xml = Builder::XmlMarkup.new :indent => 2 build_merchant_data(xml, data) xml.target! end def merchant_data_xml(data) xml = Builder::XmlMarkup.new build_merchant_data(xml, data) xml.target! end def build_merchant_data(xml, data) xml.DATOSENTRADA do # Basic elements xml.DS_Version 0.1 xml.DS_MERCHANT_CURRENCY data[:currency] xml.DS_MERCHANT_AMOUNT data[:amount] xml.DS_MERCHANT_ORDER data[:order_id] xml.DS_MERCHANT_TRANSACTIONTYPE data[:action] xml.DS_MERCHANT_PRODUCTDESCRIPTION data[:description] xml.DS_MERCHANT_TERMINAL @options[:terminal] xml.DS_MERCHANT_MERCHANTCODE @options[:login] xml.DS_MERCHANT_MERCHANTSIGNATURE build_signature(data) unless sha256_authentication? # Only when card is present if data[:card] xml.DS_MERCHANT_TITULAR data[:card][:name] xml.DS_MERCHANT_PAN data[:card][:pan] xml.DS_MERCHANT_EXPIRYDATE data[:card][:date] xml.DS_MERCHANT_CVV2 data[:card][:cvv] xml.DS_MERCHANT_IDENTIFIER 'REQUIRED' if data[:store_in_vault] elsif data[:credit_card_token] xml.DS_MERCHANT_IDENTIFIER data[:credit_card_token] end end end def parse(data) params = {} success = false message = "" options = @options.merge(:test => test?) xml = Nokogiri::XML(data) code = xml.xpath("//RETORNOXML/CODIGO").text if code == "0" op = xml.xpath("//RETORNOXML/OPERACION") op.children.each do |element| params[element.name.downcase.to_sym] = element.text end if validate_signature(params) message = response_text(params[:ds_response]) options[:authorization] = build_authorization(params) success = is_success_response?(params[:ds_response]) else message = "Response failed validation check" end else # Some kind of programmer error with the request! message = "#{code} ERROR" end Response.new(success, message, params, options) end def validate_signature(data) if sha256_authentication? sig = Base64.strict_encode64(mac256(get_key(data[:ds_order].to_s), xml_signed_fields(data))) sig.upcase == data[:ds_signature].to_s.upcase else str = data[:ds_amount] + data[:ds_order].to_s + data[:ds_merchantcode] + data[:ds_currency] + data[:ds_response] + data[:ds_cardnumber].to_s + data[:ds_transactiontype].to_s + data[:ds_securepayment].to_s + @options[:secret_key] sig = Digest::SHA1.hexdigest(str) data[:ds_signature].to_s.downcase == sig end end def build_authorization(params) [params[:ds_order], params[:ds_amount], params[:ds_currency]].join("|") end def split_authorization(authorization) order_id, amount, currency = authorization.split("|") [order_id, amount.to_i, currency] end def currency_code(currency) return currency if currency =~ /^\d+$/ raise ArgumentError, "Unknown currency #{currency}" unless CURRENCY_CODES[currency] CURRENCY_CODES[currency] end def transaction_code(type) SUPPORTED_TRANSACTIONS[type] end def response_text(code) code = code.to_i code = 0 if code < 100 RESPONSE_TEXTS[code] || "Unkown code, please check in manual" end def is_success_response?(code) (code.to_i < 100) || [400, 481, 500, 900].include?(code.to_i) end def clean_order_id(order_id) cleansed = order_id.gsub(/[^\da-zA-Z]/, '') if cleansed =~ /^\d{4}/ cleansed[0..11] else "%04d%s" % [rand(0..9999), cleansed[0...8]] end end def sha256_authentication? @options[:signature_algorithm] == "sha256" end def sign_request(xml_request_string, order_id) key = encrypt(@options[:secret_key], order_id) Base64.strict_encode64(mac256(key, xml_request_string)) end def encrypt(key, order_id) block_length = 8 cipher = OpenSSL::Cipher.new('DES3') cipher.encrypt cipher.key = Base64.strict_decode64(key) # The OpenSSL default of an all-zeroes ("\\0") IV is used. cipher.padding = 0 order_id += "\0" until order_id.bytesize % block_length == 0 # Pad with zeros output = cipher.update(order_id) + cipher.final output end def mac256(key, data) OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), key, data) end def xml_signed_fields(data) data[:ds_amount] + data[:ds_order] + data[:ds_merchantcode] + data[:ds_currency] + data[:ds_response] + data[:ds_transactiontype] + data[:ds_securepayment] end def get_key(order_id) encrypt(@options[:secret_key], order_id) end end end end