diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4d0e188..a09f8e8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,23 +3,36 @@ name: Test on: [push, pull_request] jobs: + rubocop: + runs-on: ubuntu-latest + env: + CI: true + steps: + - uses: actions/checkout@v3 + - name: Set up Ruby 3.2 + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.2 + bundler-cache: true + - name: Run RuboCop + run: bundle exec rubocop --parallel + test: - name: Unit test strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest] ruby-version: [2.6, 2.7, 3.0, 3.1, 3.2, jruby-9.4, truffleruby] runs-on: ${{ matrix.os }} + env: + CI: true steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Ruby ${{ matrix.ruby-version }} uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby-version }} - - name: Install dependencies run: bundle install - - name: Run tests run: bundle exec rspec diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..f48bd2a --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,37 @@ +inherit_from: .rubocop_todo.yml + +# inherit_from: .rubocop_todo.yml + +require: + - rubocop-performance + - rubocop-rake + - rubocop-rspec + +AllCops: + TargetRubyVersion: 2.6 + NewCops: enable + Exclude: + - 'data/**/*' + - 'vendor/**/*' + +Metrics/CollectionLiteralLength: + Exclude: + - 'lib/paygate/aes.rb' + +Naming/FileName: + Exclude: + - 'lib/paygate-ruby.rb' + +Naming/MethodParameterName: + Exclude: + - 'lib/paygate/aes.rb' + - 'lib/paygate/aes_ctr.rb' + +RSpec/NotToNot: + EnforcedStyle: to_not + +Style/Documentation: + Enabled: false + +Style/ModuleFunction: + EnforcedStyle: extend_self diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml new file mode 100644 index 0000000..53a7ed1 --- /dev/null +++ b/.rubocop_todo.yml @@ -0,0 +1,27 @@ +# This configuration was generated by +# `rubocop --auto-gen-config` +# on 2023-04-26 08:36:11 UTC using RuboCop version 1.50.0. +# The point is for the user to remove these configuration records +# one by one as the offenses are removed from the code base. +# Note that changes in the inspected code, or installation of new +# versions of RuboCop, may require this file to be generated again. + +# Offense count: 7 +# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. +Metrics/AbcSize: + Max: 93 + +# Offense count: 1 +# Configuration parameters: CountComments, CountAsOne. +Metrics/ClassLength: + Max: 113 + +# Offense count: 8 +# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. +Metrics/MethodLength: + Max: 35 + +# Offense count: 1 +# Configuration parameters: CountComments, CountAsOne. +Metrics/ModuleLength: + Max: 123 diff --git a/CHANGELOG.md b/CHANGELOG.md index 05f61b5..7722067 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - [PR#9](https://github.com/tablecheck/paygate-ruby/pull/9) Do not include Paygate::FormHelper in ActionView::Base. ([johnnyshields](https://github.com/johnnyshields)) - [PR#9](https://github.com/tablecheck/paygate-ruby/pull/9) Update BIN list. Note BINs can now be a flexible number of digits. ([johnnyshields](https://github.com/johnnyshields)) - [PR#9](https://github.com/tablecheck/paygate-ruby/pull/9) Rename data config YAML keys. ([johnnyshields](https://github.com/johnnyshields)) +- [PR#10](https://github.com/tablecheck/paygate-ruby/pull/10) Add Rubocop and fix compliance issues. ([johnnyshields](https://github.com/johnnyshields)) ## 0.1.11 - 2022-05-13 diff --git a/Gemfile b/Gemfile index e121039..a2f8224 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + source 'https://rubygems.org' group :development, :test do @@ -6,6 +8,10 @@ end group :test do gem 'rspec' + gem 'rubocop', '~> 1.49.0' + gem 'rubocop-performance', '~> 1.16.0' + gem 'rubocop-rake', '~> 0.6.0' + gem 'rubocop-rspec', '~> 2.19.0' gem 'timecop' end diff --git a/Rakefile b/Rakefile index c702cfc..7398a90 100644 --- a/Rakefile +++ b/Rakefile @@ -1 +1,3 @@ +# frozen_string_literal: true + require 'bundler/gem_tasks' diff --git a/lib/paygate-ruby.rb b/lib/paygate-ruby.rb index 785acb2..29742c0 100644 --- a/lib/paygate-ruby.rb +++ b/lib/paygate-ruby.rb @@ -1,43 +1,3 @@ -require 'yaml' -require 'paygate/version' -require 'paygate/configuration' -require 'paygate/aes' -require 'paygate/aes_ctr' -require 'paygate/member' -require 'paygate/response' -require 'paygate/transaction' -require 'paygate/profile' -require 'paygate/action_view/form_helper' if defined?(ActionView) +# frozen_string_literal: true -module Paygate - CONFIG = YAML.load(File.read(File.expand_path('../data/config.yml', File.dirname(__FILE__)))).freeze - LOCALES_MAP = CONFIG[:locales].freeze - INTL_BRANDS_MAP = CONFIG[:intl_brands].freeze - KOREA_BIN_NUMBERS = CONFIG[:korea_bin_numbers].freeze - DEFAULT_CURRENCY = 'WON'.freeze - DEFAULT_LOCALE = 'US'.freeze - - def mapped_currency(currency) - return DEFAULT_CURRENCY unless currency.present? - - currency.to_s == 'KRW' ? 'WON' : currency.to_s - end - module_function :mapped_currency - - def mapped_locale(locale) - locale.present? ? LOCALES_MAP[locale.to_s] : DEFAULT_LOCALE - end - module_function :mapped_locale - - class << self - attr_writer :configuration - end - - def self.configuration - @configuration ||= Configuration.new - end - - def self.configure - yield configuration - end -end +require 'paygate' diff --git a/lib/paygate.rb b/lib/paygate.rb new file mode 100644 index 0000000..13a1038 --- /dev/null +++ b/lib/paygate.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'yaml' +require 'paygate/version' +require 'paygate/configuration' +require 'paygate/aes' +require 'paygate/aes_ctr' +require 'paygate/member' +require 'paygate/response' +require 'paygate/transaction' +require 'paygate/profile' +require 'paygate/action_view/form_helper' if defined?(ActionView) + +module Paygate + extend self + + CONFIG = YAML.safe_load(File.read(File.expand_path('../data/config.yml', __dir__)), + permitted_classes: [Symbol]).freeze + LOCALES_MAP = CONFIG[:locales].freeze + INTL_BRANDS_MAP = CONFIG[:intl_brands].freeze + KOREA_BIN_NUMBERS = CONFIG[:korea_bin_numbers].freeze + DEFAULT_CURRENCY = 'WON' + DEFAULT_LOCALE = 'US' + + def mapped_currency(currency) + currency = currency&.to_s + return DEFAULT_CURRENCY if currency.nil? + + currency.to_s == 'KRW' ? 'WON' : currency.to_s + end + + def mapped_locale(locale) + LOCALES_MAP[locale&.to_s] || DEFAULT_LOCALE + end + + def configuration + @configuration ||= Configuration.new + end + + def configure + yield(configuration) + end +end diff --git a/lib/paygate/action_view/form_helper.rb b/lib/paygate/action_view/form_helper.rb index 5cd434a..5e52081 100644 --- a/lib/paygate/action_view/form_helper.rb +++ b/lib/paygate/action_view/form_helper.rb @@ -1,155 +1,159 @@ -module Paygate -module ActionView - module FormHelper - PAYGATE_FORM_TEXT_FIELDS = { - mid: { - placeholder: 'Member ID' - }, - - locale: { - name: 'langcode', - default: 'KR', - placeholder: 'Language' - }, - - charset: { - default: 'UTF-8', - placeholder: 'Charset' - }, - - title: { - name: 'goodname', - placeholder: 'Title' - }, - - currency: { - name: 'goodcurrency', - default: 'WON', - placeholder: 'Currency' - }, - - amount: { - name: 'unitprice', - placeholder: 'Amount' - }, - - meta1: { - name: 'goodoption1', - placeholder: 'Good Option 1' - }, - - meta2: { - name: 'goodoption2', - placeholder: 'Good Option 2' - }, - - meta3: { - name: 'goodoption3', - placeholder: 'Good Option 3' - }, - - meta4: { - name: 'goodoption4', - placeholder: 'Good Option 4' - }, - - meta5: { - name: 'goodoption5', - placeholder: 'Good Option 5' - }, - - pay_method: { - name: 'paymethod', - default: 'card', - placeholder: 'Pay Method' - }, - - customer_name: { - name: 'receipttoname', - placeholder: 'Customer Name' - }, - - customer_email: { - name: 'receipttoemail', - placeholder: 'Customer Email' - }, - - card_number: { - name: 'cardnumber', - placeholder: 'Card Number' - }, - - expiry_year: { - name: 'cardexpireyear', - placeholder: 'Expiry Year' - }, - - expiry_month: { - name: 'cardexpiremonth', - placeholder: 'Expiry Month' - }, - - cvv: { - name: 'cardsecretnumber', - placeholder: 'CVV' - }, - - card_auth_code: { - name: 'cardauthcode', - placeholder: 'Card Auth Code' - }, - - response_code: { - name: 'replycode', - placeholder: 'Response Code' - }, - - response_message: { - name: 'replyMsg', - placeholder: 'Response Message' - }, - - tid: { - placeholder: 'TID' - }, - - profile_no: { - placeholder: 'Profile No' - }, - - hash_result: { - name: 'hashresult', - placeholder: 'Hash Result' - } - } - - def paygate_open_pay_api_js_url - (Paygate.configuration.mode == :live) ? - 'https://api.paygate.net/ajax/common/OpenPayAPI.js'.freeze : - 'https://stgapi.paygate.net/ajax/common/OpenPayAPI.js'.freeze - end +# frozen_string_literal: true - def paygate_open_pay_api_form(options = {}) - form_tag({}, name: 'PGIOForm') do - fields = [] - - PAYGATE_FORM_TEXT_FIELDS.each do |key, opts| - arg_opts = options[key] || {} - fields << text_field_tag( - key, - arg_opts[:value] || opts[:default], - name: opts[:name] || key.to_s, - placeholder: arg_opts[:placeholder] || opts[:placeholder] - ).html_safe +module Paygate + module ActionView + module FormHelper + PAYGATE_FORM_TEXT_FIELDS = { + mid: { + placeholder: 'Member ID' + }, + + locale: { + name: 'langcode', + default: 'KR', + placeholder: 'Language' + }, + + charset: { + default: 'UTF-8', + placeholder: 'Charset' + }, + + title: { + name: 'goodname', + placeholder: 'Title' + }, + + currency: { + name: 'goodcurrency', + default: 'WON', + placeholder: 'Currency' + }, + + amount: { + name: 'unitprice', + placeholder: 'Amount' + }, + + meta1: { + name: 'goodoption1', + placeholder: 'Good Option 1' + }, + + meta2: { + name: 'goodoption2', + placeholder: 'Good Option 2' + }, + + meta3: { + name: 'goodoption3', + placeholder: 'Good Option 3' + }, + + meta4: { + name: 'goodoption4', + placeholder: 'Good Option 4' + }, + + meta5: { + name: 'goodoption5', + placeholder: 'Good Option 5' + }, + + pay_method: { + name: 'paymethod', + default: 'card', + placeholder: 'Pay Method' + }, + + customer_name: { + name: 'receipttoname', + placeholder: 'Customer Name' + }, + + customer_email: { + name: 'receipttoemail', + placeholder: 'Customer Email' + }, + + card_number: { + name: 'cardnumber', + placeholder: 'Card Number' + }, + + expiry_year: { + name: 'cardexpireyear', + placeholder: 'Expiry Year' + }, + + expiry_month: { + name: 'cardexpiremonth', + placeholder: 'Expiry Month' + }, + + cvv: { + name: 'cardsecretnumber', + placeholder: 'CVV' + }, + + card_auth_code: { + name: 'cardauthcode', + placeholder: 'Card Auth Code' + }, + + response_code: { + name: 'replycode', + placeholder: 'Response Code' + }, + + response_message: { + name: 'replyMsg', + placeholder: 'Response Message' + }, + + tid: { + placeholder: 'TID' + }, + + profile_no: { + placeholder: 'Profile No' + }, + + hash_result: { + name: 'hashresult', + placeholder: 'Hash Result' + } + }.freeze + + def paygate_open_pay_api_js_url + if Paygate.configuration.mode == :live + 'https://api.paygate.net/ajax/common/OpenPayAPI.js' + else + 'https://stgapi.paygate.net/ajax/common/OpenPayAPI.js' end - - fields.join.html_safe - end.html_safe - end - - def paygate_open_pay_api_screen - content_tag(:div, nil, id: 'PGIOscreen') + end + + def paygate_open_pay_api_form(options = {}) + form_tag({}, name: 'PGIOForm') do + fields = [] + + PAYGATE_FORM_TEXT_FIELDS.each do |key, opts| + arg_opts = options[key] || {} + fields << text_field_tag( + key, + arg_opts[:value] || opts[:default], + name: opts[:name] || key.to_s, + placeholder: arg_opts[:placeholder] || opts[:placeholder] + ).html_safe + end + + fields.join.html_safe + end.html_safe + end + + def paygate_open_pay_api_screen + content_tag(:div, nil, id: 'PGIOscreen') + end end end end -end diff --git a/lib/paygate/aes.rb b/lib/paygate/aes.rb index d6efb07..9ff432a 100644 --- a/lib/paygate/aes.rb +++ b/lib/paygate/aes.rb @@ -1,22 +1,24 @@ +# frozen_string_literal: true + module Paygate class Aes # Pre-computed multiplicative inverse in GF(2^8) - S_BOX = [0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76, - 0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0, - 0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15, - 0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75, - 0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84, - 0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf, - 0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8, - 0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2, - 0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73, - 0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb, - 0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79, - 0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08, - 0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a, - 0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e, - 0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf, - 0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16].freeze + S_BOX = [0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, + 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, + 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, + 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, + 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, + 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, + 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, + 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, + 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, + 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, + 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, + 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, + 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, + 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, + 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, + 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16].freeze # Round Constant used for the Key Expansion [1st col is 2^(r-1) in GF(2^8)] R_CON = [[0x00, 0x00, 0x00, 0x00], @@ -29,10 +31,10 @@ class Aes [0x40, 0x00, 0x00, 0x00], [0x80, 0x00, 0x00, 0x00], [0x1b, 0x00, 0x00, 0x00], - [0x36, 0x00, 0x00, 0x00] ].freeze + [0x36, 0x00, 0x00, 0x00]].freeze # Block size (in words): no of columns in state (fixed for AES) - CIPHER_BLOCK_SIZE = 4.freeze + CIPHER_BLOCK_SIZE = 4 # AES Cipher function: encrypt 'input' state with Rijndael algorithm # applies Nr rounds (10/12/14) using key schedule w for 'add round key' stage @@ -40,8 +42,8 @@ class Aes # @param int[][] w Key schedule as 2D byte-array (Nr+1 x Nb bytes) # @returns int[] Encrypted output state array def self.cipher(input, w) - nr = w.length / CIPHER_BLOCK_SIZE - 1 # no of rounds: 10/12/14 for 128/192/256-bit keys - state = [[],[],[],[]] # initialize 4x4 byte-array 'state' with input + nr = (w.length / CIPHER_BLOCK_SIZE) - 1 # no of rounds: 10/12/14 for 128/192/256-bit keys + state = [[], [], [], []] # initialize 4x4 byte-array 'state' with input (0...(4 * CIPHER_BLOCK_SIZE)).each do |i| state[i % 4][(i / 4.0).floor] = input[i] @@ -74,17 +76,17 @@ def self.key_expansion(key) w = [] temp = [] - 0.upto(nk - 1){ |i| w[i] = [key[4 * i], key[4 * i + 1], key[4 * i + 2], key[4 * i + 3]] } + 0.upto(nk - 1) { |i| w[i] = [key[4 * i], key[(4 * i) + 1], key[(4 * i) + 2], key[(4 * i) + 3]] } (nk...(CIPHER_BLOCK_SIZE * (nr + 1))).each do |i| w[i] = [] - 0.upto(3){ |t| temp[t] = w[i - 1][t] } - if i % nk == 0 + 0.upto(3) { |t| temp[t] = w[i - 1][t] } + if (i % nk).zero? temp = sub_word(rotate_word(temp)) - 0.upto(3){ |t| temp[t] ^= R_CON[i / nk][t]} + 0.upto(3) { |t| temp[t] ^= R_CON[i / nk][t] } elsif nk > 6 && i % nk == 4 temp = sub_word(temp) end - 0.upto(3){ |t| w[i][t] = w[i - nk][t] ^ temp[t] } + 0.upto(3) { |t| w[i][t] = w[i - nk][t] ^ temp[t] } end w end @@ -92,7 +94,7 @@ def self.key_expansion(key) # apply SBox to state def self.sub_bytes(state) 0.upto(3) do |r| - 0.upto(CIPHER_BLOCK_SIZE - 1){ |c| state[r][c] = S_BOX[state[r][c]] } + 0.upto(CIPHER_BLOCK_SIZE - 1) { |c| state[r][c] = S_BOX[state[r][c]] } end state end @@ -101,20 +103,20 @@ def self.sub_bytes(state) def self.shift_rows(state) t = [] 1.upto(3) do |r| - 0.upto(3){ |c| t[c] = state[r][(c + r) % CIPHER_BLOCK_SIZE] } # shift into temp copy - 0.upto(3){ |c| state[r][c] = t[c] } # and copy back - end # note that this will work for nb = 4,5,6, but not 7,8 (always 4 for AES): + 0.upto(3) { |c| t[c] = state[r][(c + r) % CIPHER_BLOCK_SIZE] } # shift into temp copy + 0.upto(3) { |c| state[r][c] = t[c] } # and copy back + end state # see asmaes.sourceforge.net/rijndael/rijndaelImplementation.pdf end # combine bytes of each col of state S def self.mix_columns(state) 0.upto(3) do |c| - a=[] # 'a' is a copy of the current column from 'state' - b=[] # 'b' is a•{02} in GF(2^8) + a = [] # 'a' is a copy of the current column from 'state' + b = [] # 'b' is a•{02} in GF(2^8) 0.upto(3) do |i| a[i] = state[i][c] - b[i] = (state[i][c] & 0x80 == 0) ? (state[i][c] << 1) ^ 0x011b : state[i][c] << 1 + b[i] = (state[i][c] & 0x80).zero? ? (state[i][c] << 1) ^ 0x011b : state[i][c] << 1 end # a[n] ^ b[n] is a•{03} in GF(2^8) state[0][c] = b[0] ^ a[1] ^ b[1] ^ a[2] ^ a[3] # 2*a0 + 3*a1 + a2 + a3 @@ -128,21 +130,21 @@ def self.mix_columns(state) # xor Round Key into state S def self.add_round_key(state, w, rnd) 0.upto(3) do |r| - 0.upto(CIPHER_BLOCK_SIZE - 1){ |c| state[r][c] ^= w[rnd * 4 + c][r]} + 0.upto(CIPHER_BLOCK_SIZE - 1) { |c| state[r][c] ^= w[(rnd * 4) + c][r] } end state end # apply SBox to 4-byte word w def self.sub_word(word) - 0.upto(3){ |i| word[i] = S_BOX[word[i]] } + 0.upto(3) { |i| word[i] = S_BOX[word[i]] } word end # rotate 4-byte word w left by one byte def self.rotate_word(word) tmp = word[0] - 0.upto(2){ |i| word[i] = word[i + 1] } + 0.upto(2) { |i| word[i] = word[i + 1] } word[3] = tmp word end diff --git a/lib/paygate/aes_ctr.rb b/lib/paygate/aes_ctr.rb index 1071da0..968f38e 100644 --- a/lib/paygate/aes_ctr.rb +++ b/lib/paygate/aes_ctr.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true + require 'base64' module Paygate class AesCtr - # Encrypt a text using AES encryption in Counter mode of operation # # Unicode multi-byte character safe @@ -12,16 +13,19 @@ class AesCtr # @param int num_bits Number of bits to be used in the key (128, 192, or 256) # @returns string Encrypted text def self.encrypt(plaintext, password, num_bits) - block_size = 16 # block size fixed at 16 bytes / 128 bits (Nb=4) for AES + block_size = 16 # block size fixed at 16 bytes / 128 bits (Nb=4) for AES return '' unless [128, 192, 256].include?(num_bits) # use AES itself to encrypt password to get cipher key (using plain password as source for key # expansion) - gives us well encrypted key (though hashed key might be preferred for prod'n use) - num_bytes = num_bits / 8 # no bytes in key (16/24/32) + num_bytes = num_bits / 8 # no bytes in key (16/24/32) pw_bytes = [] - 0.upto(num_bytes - 1){ |i| pw_bytes[i] = password.bytes.to_a[i] & 0xff || 0} # use 1st 16/24/32 chars of password for key #warn - key = Aes.cipher(pw_bytes, Aes.key_expansion(pw_bytes)) # gives us 16-byte key - key = key + key[0, num_bytes - 16] # expand key to 16/24/32 bytes long + # use 1st 16/24/32 chars of password for key #warn + 0.upto(num_bytes - 1) do |i| + pw_bytes[i] = (password.bytes.to_a[i] & 0xff) || 0 + end + key = Aes.cipher(pw_bytes, Aes.key_expansion(pw_bytes)) # gives us 16-byte key + key += key[0, num_bytes - 16] # expand key to 16/24/32 bytes long # initialise 1st 8 bytes of counter block with nonce (NIST SP800-38A §B.2): [0-1] = millisec, # [2-3] = random, [4-7] = seconds, together giving full sub-millisec uniqueness up to Feb 2106 @@ -29,14 +33,14 @@ def self.encrypt(plaintext, password, num_bits) nonce = Time.now.to_i nonce_ms = nonce % 1000 nonce_sec = (nonce / 1000.0).floor - nonce_rand = (rand() * 0xffff).floor - 0.upto(1){ |i| counter_block[i] = urs(nonce_ms, i * 8) & 0xff } - 0.upto(1){ |i| counter_block[i + 2] = urs(nonce_rand, i * 8) & 0xff } - 0.upto(3){ |i| counter_block[i + 4] = urs(nonce_sec, i * 8) & 0xff } + nonce_rand = (rand * 0xffff).floor + 0.upto(1) { |i| counter_block[i] = urs(nonce_ms, i * 8) & 0xff } + 0.upto(1) { |i| counter_block[i + 2] = urs(nonce_rand, i * 8) & 0xff } + 0.upto(3) { |i| counter_block[i + 4] = urs(nonce_sec, i * 8) & 0xff } # and convert it to a string to go on the front of the ciphertext ctr_text = '' - 0.upto(7){ |i| ctr_text += counter_block[i].chr } + 0.upto(7) { |i| ctr_text += counter_block[i].chr } # generate key schedule - an expansion of the key into distinct Key Rounds for each round key_schedule = Aes.key_expansion(key) @@ -46,85 +50,84 @@ def self.encrypt(plaintext, password, num_bits) 0.upto(block_count - 1) do |b| # set counter (block #) in last 8 bytes of counter block (leaving nonce in 1st 8 bytes) # done in two stages for 32-bit ops: using two words allows us to go past 2^32 blocks (68GB) - 0.upto(3){ |c| counter_block[15 - c] = urs(b, c * 8) & 0xff } - 0.upto(3){ |c| counter_block[15 - c - 4] = urs(b / 0x100000000, c * 8) } + 0.upto(3) { |c| counter_block[15 - c] = urs(b, c * 8) & 0xff } + 0.upto(3) { |c| counter_block[15 - c - 4] = urs(b / 0x100000000, c * 8) } cipher_cntr = Aes.cipher(counter_block, key_schedule) # -- encrypt counter block -- # block size is reduced on final block - block_length = b < block_count - 1 ? block_size : (plaintext.length - 1) % block_size + 1 + block_length = b < block_count - 1 ? block_size : ((plaintext.length - 1) % block_size) + 1 cipher_char = [] 0.upto(block_length - 1) do |i| - cipher_char[i] = (cipher_cntr[i] ^ plaintext.bytes.to_a[b * block_size + i]).chr + cipher_char[i] = (cipher_cntr[i] ^ plaintext.bytes.to_a[(b * block_size) + i]).chr end cipher_text[b] = cipher_char.join end cipher_text = ctr_text + cipher_text.join - Base64.encode64(cipher_text).gsub(/\n/, '') + "\n" # encode in base64 + "#{Base64.encode64(cipher_text).delete("\n")}\n" # encode in base64 end # Decrypt a text encrypted by AES in counter mode of operation # # @param string ciphertext Source text to be encrypted # @param string password The password to use to generate a key - # @param int nBits Number of bits to be used in the key (128, 192, or 256) + # @param int n_bits Number of bits to be used in the key (128, 192, or 256) # @returns string # Decrypted text - def self.decrypt(ciphertext, password, nBits) - blockSize = 16 # block size fixed at 16 bytes / 128 bits (Nb=4) for AES - return '' unless(nBits==128 || nBits==192 || nBits==256) + def self.decrypt(ciphertext, password, n_bits) + block_size = 16 # block size fixed at 16 bytes / 128 bits (Nb=4) for AES + return '' unless [128, 192, 256].include?(n_bits) + ciphertext = Base64.decode64(ciphertext) - nBytes = nBits/8 # no bytes in key (16/24/32) - pwBytes = [] - 0.upto(nBytes-1){|i| pwBytes[i] = password.bytes.to_a[i] & 0xff || 0} - key = Aes.cipher(pwBytes, Aes.key_expansion(pwBytes)) # gives us 16-byte key - key = key.concat(key.slice(0, nBytes-16)) # expand key to 16/24/32 bytes long + n_bytes = n_bits / 8 # no bytes in key (16/24/32) + pw_bytes = [] + 0.upto(n_bytes - 1) { |i| pw_bytes[i] = (password.bytes.to_a[i] & 0xff) || 0 } + key = Aes.cipher(pw_bytes, Aes.key_expansion(pw_bytes)) # gives us 16-byte key + key.concat(key.slice(0, n_bytes - 16)) # expand key to 16/24/32 bytes long # recover nonce from 1st 8 bytes of ciphertext - counterBlock = [] - ctrTxt = ciphertext[0,8] - 0.upto(7){|i| counterBlock[i] = ctrTxt.bytes.to_a[i]} + counter_block = [] + ctr_txt = ciphertext[0, 8] + 0.upto(7) { |i| counter_block[i] = ctr_txt.bytes.to_a[i] } - #generate key Schedule - keySchedule = Aes.key_expansion(key) + # generate key Schedule + key_schedule = Aes.key_expansion(key) # separate ciphertext into blocks (skipping past initial 8 bytes) - nBlocks = ((ciphertext.length-8)/blockSize.to_f).ceil - ct=[] - 0.upto(nBlocks-1){|b|ct[b] = ciphertext[8+b*blockSize, 16]} + n_blocks = ((ciphertext.length - 8) / block_size.to_f).ceil + ct = [] + 0.upto(n_blocks - 1) { |b| ct[b] = ciphertext[8 + (b * block_size), 16] } - ciphertext = ct; # ciphertext is now array of block-length strings + ciphertext = ct; # ciphertext is now array of block-length strings # plaintext will get generated block-by-block into array of block-length strings plaintxt = [] - 0.upto(nBlocks-1) do |b| - 0.upto(3){|c| counterBlock[15-c] = urs(b,c*8) & 0xff} - 0.upto(3){|c| counterBlock[15-c-4] = urs((b+1)/(0x100000000-1),c*8) & 0xff} - cipherCntr = Aes.cipher(counterBlock, keySchedule) # encrypt counter block - plaintxtByte = [] + 0.upto(n_blocks - 1) do |b| + 0.upto(3) { |c| counter_block[15 - c] = urs(b, c * 8) & 0xff } + 0.upto(3) { |c| counter_block[15 - c - 4] = urs((b + 1) / (0x100000000 - 1), c * 8) & 0xff } + cipher_cntr = Aes.cipher(counter_block, key_schedule) # encrypt counter block + plaintxt_byte = [] 0.upto(ciphertext[b].length - 1) do |i| # -- xor plaintxt with ciphered counter byte-by-byte -- - plaintxtByte[i] = (cipherCntr[i] ^ ciphertext[b].bytes.to_a[i]).chr; + plaintxt_byte[i] = (cipher_cntr[i] ^ ciphertext[b].bytes.to_a[i]).chr end - plaintxt[b] = plaintxtByte.join('') + plaintxt[b] = plaintxt_byte.join end - plaintxt.join('') + plaintxt.join end - private - - # Unsigned right shift function, since Ruby has neither >>> operator nor unsigned ints - # - # @param a number to be shifted (32-bit integer) - # @param b number of bits to shift a to the right (0..31) - # @return a right-shifted and zero-filled by b bits + # Unsigned right shift function, since Ruby has neither >>> operator nor unsigned ints + # + # @param a number to be shifted (32-bit integer) + # @param b number of bits to shift a to the right (0..31) + # @return a right-shifted and zero-filled by b bits def self.urs(a, b) a &= 0xffffffff b &= 0x1f - if a & 0x80000000 && b > 0 # if left-most bit set + if (a & 0x80000000) && b.positive? # if left-most bit set a = ((a >> 1) & 0x7fffffff) # right-shift one bit & clear left-most bit a = a >> (b - 1) # remaining right-shifts - else # otherwise + else # otherwise a = (a >> b); # use normal right-shift end a diff --git a/lib/paygate/configuration.rb b/lib/paygate/configuration.rb index 8164772..c8e217a 100644 --- a/lib/paygate/configuration.rb +++ b/lib/paygate/configuration.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Paygate class Configuration MODES = %i[live sandbox].freeze @@ -10,7 +12,8 @@ def initialize def mode=(value) value = value.to_sym - fail 'Invalid mode. Value must be one of the following: :live, :sandbox' unless value && MODES.include?(value) + raise 'Invalid mode. Value must be one of the following: :live, :sandbox' unless value && MODES.include?(value) + @mode = value end end diff --git a/lib/paygate/member.rb b/lib/paygate/member.rb index 6023782..8fed3b4 100644 --- a/lib/paygate/member.rb +++ b/lib/paygate/member.rb @@ -1,9 +1,12 @@ +# frozen_string_literal: true + module Paygate class Member attr_reader :mid, :secret def initialize(mid, secret) - @mid, @secret = mid, secret + @mid = mid + @secret = secret end def refund_transaction(txn_id, options = {}) diff --git a/lib/paygate/profile.rb b/lib/paygate/profile.rb index 0bf0315..303238c 100644 --- a/lib/paygate/profile.rb +++ b/lib/paygate/profile.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + require 'uri' require 'net/http' module Paygate class Profile - PURCHASE_URL = 'https://service.paygate.net/INTL/pgtlProcess3.jsp'.freeze + PURCHASE_URL = 'https://service.paygate.net/INTL/pgtlProcess3.jsp' attr_reader :profile_no attr_accessor :member diff --git a/lib/paygate/response.rb b/lib/paygate/response.rb index 6038660..cbb96d7 100644 --- a/lib/paygate/response.rb +++ b/lib/paygate/response.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Paygate class Response attr_accessor :transaction_type, :http_code, :message, :body, :raw_info, :json diff --git a/lib/paygate/transaction.rb b/lib/paygate/transaction.rb index a10af75..0e9fee8 100644 --- a/lib/paygate/transaction.rb +++ b/lib/paygate/transaction.rb @@ -1,10 +1,12 @@ +# frozen_string_literal: true + require 'digest' require 'uri' require 'net/http' module Paygate class Transaction - FULL_AMOUNT_IDENTIFIER = 'F'.freeze + FULL_AMOUNT_IDENTIFIER = 'F' attr_reader :tid attr_accessor :member @@ -15,8 +17,8 @@ def initialize(tid) def refund(options = {}) # Encrypt data - api_key_256 = ::Digest::SHA256.hexdigest(member.secret) - aes_ctr = AesCtr.encrypt(tid, api_key_256, 256) + api_key256 = ::Digest::SHA256.hexdigest(member.secret) + aes_ctr = AesCtr.encrypt(tid, api_key256, 256) tid_enc = "AES256#{aes_ctr}" # Prepare params @@ -32,7 +34,7 @@ def refund(options = {}) response = ::Net::HTTP.get_response(uri) r = Response.build_from_net_http_response(:refund, response) - r.raw_info = OpenStruct.new(tid: tid, tid_enc: tid_enc, request_url: uri.to_s) + r.raw_info = OpenStruct.new(tid: tid, tid_enc: tid_enc, request_url: uri.to_s) # rubocop:disable Style/OpenStructUse r end @@ -47,12 +49,12 @@ def verify Response.build_from_net_http_response(:verify, response) end - private - def self.refund_api_url - (Paygate.configuration.mode == :live) ? - 'https://service.paygate.net/service/cancelAPI.json' : + if Paygate.configuration.mode == :live + 'https://service.paygate.net/service/cancelAPI.json' + else 'https://stgsvc.paygate.net/service/cancelAPI.json' + end end end end diff --git a/lib/paygate/version.rb b/lib/paygate/version.rb index 6e3da7d..814b10d 100644 --- a/lib/paygate/version.rb +++ b/lib/paygate/version.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Paygate VERSION = '0.2.0' end diff --git a/paygate-ruby.gemspec b/paygate-ruby.gemspec index 0777471..7200142 100644 --- a/paygate-ruby.gemspec +++ b/paygate-ruby.gemspec @@ -1,5 +1,6 @@ -# coding: utf-8 -lib = File.expand_path('../lib', __FILE__) +# frozen_string_literal: true + +lib = File.expand_path('lib', __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'paygate/version' @@ -12,7 +13,8 @@ Gem::Specification.new do |spec| spec.summary = 'Ruby wrapper for PayGate Korea payment gateway' spec.license = 'MIT' + spec.required_ruby_version = '>= 2.6.0' spec.files = Dir.glob('{data,lib,vendor}/**/*') + %w[CHANGELOG.md LICENSE.txt README.md Rakefile] - spec.test_files = Dir.glob('spec/**/*') spec.require_paths = ['lib'] + spec.metadata['rubygems_mfa_required'] = 'true' end diff --git a/spec/integration/paygate_spec.rb b/spec/integration/paygate_spec.rb index 3fd3deb..230e340 100644 --- a/spec/integration/paygate_spec.rb +++ b/spec/integration/paygate_spec.rb @@ -2,8 +2,8 @@ require_relative '../spec_helper' -RSpec.describe 'basic' do +RSpec.describe Paygate do it 'can load gem' do - expect(true).to eq true + expect(described_class).to_not be_nil end end diff --git a/spec/paygate/aes_ctr_spec.rb b/spec/paygate/aes_ctr_spec.rb new file mode 100644 index 0000000..b6b2c31 --- /dev/null +++ b/spec/paygate/aes_ctr_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require_relative '../spec_helper' + +RSpec.describe Paygate::AesCtr do + let(:plaintext) { 'This is a test message' } + let(:password) { 'password' } + let(:num_bits) { 128 } + let(:encrypted_text) { "yACaHyMKGQCJtHL8KR0loRQOGV5zIjbGHltR8QL4\n" } + let(:invalid_num_bits) { 2560 } + + describe '.encrypt' do + before do + allow(Object).to receive(:rand).and_return(0.123456789) + end + + around do |example| + Timecop.freeze(Time.utc(2022, 1, 1)) { example.run } + end + + it 'returns an encrypted text string' do + expect(described_class.encrypt(plaintext, password, num_bits)).to eq(encrypted_text) + end + + it 'returns an empty string if num_bits is invalid' do + expect(described_class.encrypt(plaintext, password, invalid_num_bits)).to eq('') + end + end + + describe '.decrypt' do + it 'returns a decrypted text string' do + decrypted_text = described_class.decrypt(encrypted_text, password, num_bits) + expect(decrypted_text).to eq(plaintext) + end + + it 'returns an empty string if num_bits is invalid' do + expect(described_class.decrypt(encrypted_text, password, invalid_num_bits)).to eq('') + end + end +end diff --git a/spec/unit/aes_spec.rb b/spec/paygate/aes_spec.rb similarity index 90% rename from spec/unit/aes_spec.rb rename to spec/paygate/aes_spec.rb index 31565fe..188391f 100644 --- a/spec/unit/aes_spec.rb +++ b/spec/paygate/aes_spec.rb @@ -1,19 +1,21 @@ +# frozen_string_literal: true + require_relative '../spec_helper' RSpec.describe Paygate::Aes do - let(:key_128) { '0123456789abcdef' } - let(:key_192) { '0123456789abcdef012345' } - let(:key_256) { '0123456789abcdef0123456789abcdef' } + let(:key128) { '0123456789abcdef' } + let(:key192) { '0123456789abcdef012345' } + let(:key256) { '0123456789abcdef0123456789abcdef' } describe '.cipher' do context 'when encrypting with 128-bit key' do it 'encrypts the input correctly' do input = '0123456789abcdef' expected_output = '72727e881edcfd0100a718687909b565' - key_schedule = described_class.key_expansion(key_128.bytes) + key_schedule = described_class.key_expansion(key128.bytes) output = described_class.cipher(input.bytes, key_schedule) - expect(output.pack('c*').unpack('H*').first).to eq(expected_output) + expect(output.pack('c*').unpack1('H*')).to eq(expected_output) end end @@ -21,10 +23,10 @@ it 'encrypts the input correctly' do input = '0123456789abcdef01234567' expected_output = '943cb7b4f5ec4afcbd2973b72ba25e4a' - key_schedule = described_class.key_expansion(key_192.bytes) + key_schedule = described_class.key_expansion(key192.bytes) output = described_class.cipher(input.bytes, key_schedule) - expect(output.pack('c*').unpack('H*').first).to eq(expected_output) + expect(output.pack('c*').unpack1('H*')).to eq(expected_output) end end @@ -32,10 +34,10 @@ it 'encrypts the input correctly' do input = '0123456789abcdef0123456789abcdef' expected_output = 'f83c9a60dc0cdb98219f79d6d5db1635' - key_schedule = described_class.key_expansion(key_256.bytes) + key_schedule = described_class.key_expansion(key256.bytes) output = described_class.cipher(input.bytes, key_schedule) - expect(output.pack('c*').unpack('H*').first).to eq(expected_output) + expect(output.pack('c*').unpack1('H*')).to eq(expected_output) end end end @@ -92,7 +94,7 @@ end it 'returns the expected key schedule' do - key = key_128.bytes + key = key128.bytes schedule = described_class.key_expansion(key) expect(schedule).to eq(expected_schedule) end @@ -153,7 +155,7 @@ end it 'returns the expected key schedule' do - key = key_192.bytes + key = key192.bytes schedule = described_class.key_expansion(key) expect(schedule).to eq(expected_schedule) end @@ -226,7 +228,7 @@ end it 'returns the expected key schedule' do - key = key_256.bytes + key = key256.bytes schedule = described_class.key_expansion(key) expect(schedule).to eq(expected_schedule) end diff --git a/spec/unit/aes_ctr_spec.rb b/spec/unit/aes_ctr_spec.rb deleted file mode 100644 index 75d4d5a..0000000 --- a/spec/unit/aes_ctr_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -require_relative '../spec_helper' - -RSpec.describe Paygate::AesCtr do - let(:plaintext) { "This is a test message" } - let(:password) { "password" } - let(:num_bits) { 128 } - let(:encrypted_text) { "yACaHyMKGQCJtHL8KR0loRQOGV5zIjbGHltR8QL4\n" } - let(:invalid_num_bits) { 2560 } - - describe ".encrypt" do - before do - allow(Object).to receive(:rand).and_return(0.123456789) - end - - around do |example| - Timecop.freeze(Time.utc(2022, 1, 1)) { example.run } - end - - it "returns an encrypted text string" do - expect(Paygate::AesCtr.encrypt(plaintext, password, num_bits)).to eq(encrypted_text) - end - - it "returns an empty string if num_bits is invalid" do - expect(Paygate::AesCtr.encrypt(plaintext, password, invalid_num_bits)).to eq('') - end - end - - describe ".decrypt" do - it "returns a decrypted text string" do - decrypted_text = Paygate::AesCtr.decrypt(encrypted_text, password, num_bits) - expect(decrypted_text).to eq(plaintext) - end - - it "returns an empty string if num_bits is invalid" do - expect(Paygate::AesCtr.decrypt(encrypted_text, password, invalid_num_bits)).to eq('') - end - end -end