paygate-ruby 0.1.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +53 -38
- data/LICENSE.txt +21 -21
- data/README.md +327 -343
- data/Rakefile +3 -2
- data/data/cdbn.20230401.unl.xlsx +0 -0
- data/data/config.yml +3347 -2744
- data/lib/paygate/action_view/form_helper.rb +159 -0
- data/lib/paygate/aes.rb +152 -150
- data/lib/paygate/aes_ctr.rb +136 -133
- data/lib/paygate/configuration.rb +20 -16
- data/lib/paygate/member.rb +24 -21
- data/lib/paygate/profile.rb +33 -30
- data/lib/paygate/response.rb +27 -25
- data/lib/paygate/transaction.rb +60 -58
- data/lib/paygate/version.rb +5 -3
- data/lib/paygate-ruby.rb +3 -48
- data/lib/paygate.rb +43 -0
- data/vendor/assets/javascripts/paygate.js +86 -86
- metadata +17 -49
- data/.gitignore +0 -9
- data/CODE_OF_CONDUCT.md +0 -74
- data/Gemfile +0 -4
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/data/card_bin_20191001.xlsx +0 -0
- data/lib/paygate/helpers/form_helper.rb +0 -155
- data/paygate-ruby.gemspec +0 -24
data/lib/paygate/aes_ctr.rb
CHANGED
@@ -1,133 +1,136 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
# @param string
|
12
|
-
# @param
|
13
|
-
# @
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
#
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
0.upto(
|
40
|
-
|
41
|
-
#
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
#
|
71
|
-
#
|
72
|
-
#
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
#
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
a = (a >>
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'base64'
|
4
|
+
|
5
|
+
module Paygate
|
6
|
+
class AesCtr
|
7
|
+
# Encrypt a text using AES encryption in Counter mode of operation
|
8
|
+
#
|
9
|
+
# Unicode multi-byte character safe
|
10
|
+
#
|
11
|
+
# @param string plaintext Source text to be encrypted
|
12
|
+
# @param string password The password to use to generate a key
|
13
|
+
# @param int num_bits Number of bits to be used in the key (128, 192, or 256)
|
14
|
+
# @returns string Encrypted text
|
15
|
+
def self.encrypt(plaintext, password, num_bits)
|
16
|
+
block_size = 16 # block size fixed at 16 bytes / 128 bits (Nb=4) for AES
|
17
|
+
return '' unless [128, 192, 256].include?(num_bits)
|
18
|
+
|
19
|
+
# use AES itself to encrypt password to get cipher key (using plain password as source for key
|
20
|
+
# expansion) - gives us well encrypted key (though hashed key might be preferred for prod'n use)
|
21
|
+
num_bytes = num_bits / 8 # no bytes in key (16/24/32)
|
22
|
+
pw_bytes = []
|
23
|
+
# use 1st 16/24/32 chars of password for key #warn
|
24
|
+
0.upto(num_bytes - 1) do |i|
|
25
|
+
pw_bytes[i] = (password.bytes.to_a[i] & 0xff) || 0
|
26
|
+
end
|
27
|
+
key = Aes.cipher(pw_bytes, Aes.key_expansion(pw_bytes)) # gives us 16-byte key
|
28
|
+
key += key[0, num_bytes - 16] # expand key to 16/24/32 bytes long
|
29
|
+
|
30
|
+
# initialise 1st 8 bytes of counter block with nonce (NIST SP800-38A §B.2): [0-1] = millisec,
|
31
|
+
# [2-3] = random, [4-7] = seconds, together giving full sub-millisec uniqueness up to Feb 2106
|
32
|
+
counter_block = []
|
33
|
+
nonce = Time.now.to_i
|
34
|
+
nonce_ms = nonce % 1000
|
35
|
+
nonce_sec = (nonce / 1000.0).floor
|
36
|
+
nonce_rand = (rand * 0xffff).floor
|
37
|
+
0.upto(1) { |i| counter_block[i] = urs(nonce_ms, i * 8) & 0xff }
|
38
|
+
0.upto(1) { |i| counter_block[i + 2] = urs(nonce_rand, i * 8) & 0xff }
|
39
|
+
0.upto(3) { |i| counter_block[i + 4] = urs(nonce_sec, i * 8) & 0xff }
|
40
|
+
|
41
|
+
# and convert it to a string to go on the front of the ciphertext
|
42
|
+
ctr_text = ''
|
43
|
+
0.upto(7) { |i| ctr_text += counter_block[i].chr }
|
44
|
+
|
45
|
+
# generate key schedule - an expansion of the key into distinct Key Rounds for each round
|
46
|
+
key_schedule = Aes.key_expansion(key)
|
47
|
+
block_count = (plaintext.length / block_size.to_f).ceil
|
48
|
+
|
49
|
+
cipher_text = []
|
50
|
+
0.upto(block_count - 1) do |b|
|
51
|
+
# set counter (block #) in last 8 bytes of counter block (leaving nonce in 1st 8 bytes)
|
52
|
+
# done in two stages for 32-bit ops: using two words allows us to go past 2^32 blocks (68GB)
|
53
|
+
0.upto(3) { |c| counter_block[15 - c] = urs(b, c * 8) & 0xff }
|
54
|
+
0.upto(3) { |c| counter_block[15 - c - 4] = urs(b / 0x100000000, c * 8) }
|
55
|
+
|
56
|
+
cipher_cntr = Aes.cipher(counter_block, key_schedule) # -- encrypt counter block --
|
57
|
+
# block size is reduced on final block
|
58
|
+
block_length = b < block_count - 1 ? block_size : ((plaintext.length - 1) % block_size) + 1
|
59
|
+
cipher_char = []
|
60
|
+
0.upto(block_length - 1) do |i|
|
61
|
+
cipher_char[i] = (cipher_cntr[i] ^ plaintext.bytes.to_a[(b * block_size) + i]).chr
|
62
|
+
end
|
63
|
+
cipher_text[b] = cipher_char.join
|
64
|
+
end
|
65
|
+
|
66
|
+
cipher_text = ctr_text + cipher_text.join
|
67
|
+
"#{Base64.encode64(cipher_text).delete("\n")}\n" # encode in base64
|
68
|
+
end
|
69
|
+
|
70
|
+
# Decrypt a text encrypted by AES in counter mode of operation
|
71
|
+
#
|
72
|
+
# @param string ciphertext Source text to be encrypted
|
73
|
+
# @param string password The password to use to generate a key
|
74
|
+
# @param int n_bits Number of bits to be used in the key (128, 192, or 256)
|
75
|
+
# @returns string
|
76
|
+
# Decrypted text
|
77
|
+
def self.decrypt(ciphertext, password, n_bits)
|
78
|
+
block_size = 16 # block size fixed at 16 bytes / 128 bits (Nb=4) for AES
|
79
|
+
return '' unless [128, 192, 256].include?(n_bits)
|
80
|
+
|
81
|
+
ciphertext = Base64.decode64(ciphertext)
|
82
|
+
|
83
|
+
n_bytes = n_bits / 8 # no bytes in key (16/24/32)
|
84
|
+
pw_bytes = []
|
85
|
+
0.upto(n_bytes - 1) { |i| pw_bytes[i] = (password.bytes.to_a[i] & 0xff) || 0 }
|
86
|
+
key = Aes.cipher(pw_bytes, Aes.key_expansion(pw_bytes)) # gives us 16-byte key
|
87
|
+
key.concat(key.slice(0, n_bytes - 16)) # expand key to 16/24/32 bytes long
|
88
|
+
# recover nonce from 1st 8 bytes of ciphertext
|
89
|
+
counter_block = []
|
90
|
+
ctr_txt = ciphertext[0, 8]
|
91
|
+
0.upto(7) { |i| counter_block[i] = ctr_txt.bytes.to_a[i] }
|
92
|
+
|
93
|
+
# generate key Schedule
|
94
|
+
key_schedule = Aes.key_expansion(key)
|
95
|
+
|
96
|
+
# separate ciphertext into blocks (skipping past initial 8 bytes)
|
97
|
+
n_blocks = ((ciphertext.length - 8) / block_size.to_f).ceil
|
98
|
+
ct = []
|
99
|
+
0.upto(n_blocks - 1) { |b| ct[b] = ciphertext[8 + (b * block_size), 16] }
|
100
|
+
|
101
|
+
ciphertext = ct; # ciphertext is now array of block-length strings
|
102
|
+
|
103
|
+
# plaintext will get generated block-by-block into array of block-length strings
|
104
|
+
plaintxt = []
|
105
|
+
0.upto(n_blocks - 1) do |b|
|
106
|
+
0.upto(3) { |c| counter_block[15 - c] = urs(b, c * 8) & 0xff }
|
107
|
+
0.upto(3) { |c| counter_block[15 - c - 4] = urs((b + 1) / (0x100000000 - 1), c * 8) & 0xff }
|
108
|
+
cipher_cntr = Aes.cipher(counter_block, key_schedule) # encrypt counter block
|
109
|
+
plaintxt_byte = []
|
110
|
+
0.upto(ciphertext[b].length - 1) do |i|
|
111
|
+
# -- xor plaintxt with ciphered counter byte-by-byte --
|
112
|
+
plaintxt_byte[i] = (cipher_cntr[i] ^ ciphertext[b].bytes.to_a[i]).chr
|
113
|
+
end
|
114
|
+
plaintxt[b] = plaintxt_byte.join
|
115
|
+
end
|
116
|
+
plaintxt.join
|
117
|
+
end
|
118
|
+
|
119
|
+
# Unsigned right shift function, since Ruby has neither >>> operator nor unsigned ints
|
120
|
+
#
|
121
|
+
# @param a number to be shifted (32-bit integer)
|
122
|
+
# @param b number of bits to shift a to the right (0..31)
|
123
|
+
# @return a right-shifted and zero-filled by b bits
|
124
|
+
def self.urs(a, b)
|
125
|
+
a &= 0xffffffff
|
126
|
+
b &= 0x1f
|
127
|
+
if (a & 0x80000000) && b.positive? # if left-most bit set
|
128
|
+
a = ((a >> 1) & 0x7fffffff) # right-shift one bit & clear left-most bit
|
129
|
+
a = a >> (b - 1) # remaining right-shifts
|
130
|
+
else # otherwise
|
131
|
+
a = (a >> b); # use normal right-shift
|
132
|
+
end
|
133
|
+
a
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -1,16 +1,20 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Paygate
|
4
|
+
class Configuration
|
5
|
+
MODES = %i[live sandbox].freeze
|
6
|
+
|
7
|
+
attr_reader :mode
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@mode = :live
|
11
|
+
end
|
12
|
+
|
13
|
+
def mode=(value)
|
14
|
+
value = value.to_sym
|
15
|
+
raise 'Invalid mode. Value must be one of the following: :live, :sandbox' unless value && MODES.include?(value)
|
16
|
+
|
17
|
+
@mode = value
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/paygate/member.rb
CHANGED
@@ -1,21 +1,24 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Paygate
|
4
|
+
class Member
|
5
|
+
attr_reader :mid, :secret
|
6
|
+
|
7
|
+
def initialize(mid, secret)
|
8
|
+
@mid = mid
|
9
|
+
@secret = secret
|
10
|
+
end
|
11
|
+
|
12
|
+
def refund_transaction(txn_id, options = {})
|
13
|
+
txn = Transaction.new(txn_id)
|
14
|
+
txn.member = self
|
15
|
+
txn.refund(options)
|
16
|
+
end
|
17
|
+
|
18
|
+
def profile_pay(profile_no, currency, amount)
|
19
|
+
profile = Profile.new(profile_no)
|
20
|
+
profile.member = self
|
21
|
+
profile.purchase(currency, amount)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/paygate/profile.rb
CHANGED
@@ -1,30 +1,33 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'uri'
|
4
|
+
require 'net/http'
|
5
|
+
|
6
|
+
module Paygate
|
7
|
+
class Profile
|
8
|
+
PURCHASE_URL = 'https://service.paygate.net/INTL/pgtlProcess3.jsp'
|
9
|
+
|
10
|
+
attr_reader :profile_no
|
11
|
+
attr_accessor :member
|
12
|
+
|
13
|
+
def initialize(profile_no)
|
14
|
+
@profile_no = profile_no
|
15
|
+
end
|
16
|
+
|
17
|
+
def purchase(currency, amount)
|
18
|
+
# Prepare params
|
19
|
+
params = { profile_no: profile_no,
|
20
|
+
mid: member.mid,
|
21
|
+
goodcurrency: currency,
|
22
|
+
unitprice: amount }
|
23
|
+
params.compact!
|
24
|
+
|
25
|
+
# Make request
|
26
|
+
uri = URI(PURCHASE_URL)
|
27
|
+
uri.query = ::URI.encode_www_form(params)
|
28
|
+
response = ::Net::HTTP.get_response(uri)
|
29
|
+
|
30
|
+
Response.build_from_net_http_response(:profile_pay, response)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/paygate/response.rb
CHANGED
@@ -1,25 +1,27 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
r
|
9
|
-
r.
|
10
|
-
r.
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Paygate
|
4
|
+
class Response
|
5
|
+
attr_accessor :transaction_type, :http_code, :message, :body, :raw_info, :json
|
6
|
+
|
7
|
+
def self.build_from_net_http_response(txn_type, response)
|
8
|
+
r = new
|
9
|
+
r.transaction_type = txn_type
|
10
|
+
r.http_code = response.code
|
11
|
+
r.message = response.message
|
12
|
+
r.body = response.body
|
13
|
+
|
14
|
+
case txn_type
|
15
|
+
when :refund
|
16
|
+
r.json = JSON.parse response.body.gsub(/^callback\((.*)\)$/, '\1') if response.code.to_i == 200
|
17
|
+
when :profile_pay
|
18
|
+
r.json = {}
|
19
|
+
response.body.split('&').each do |key_value_pair|
|
20
|
+
key_value_ary = key_value_pair.split('=')
|
21
|
+
r.json[key_value_ary[0]] = key_value_ary[1]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
r
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/paygate/transaction.rb
CHANGED
@@ -1,58 +1,60 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require '
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
params
|
25
|
-
params
|
26
|
-
params
|
27
|
-
params
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
r
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
def self.refund_api_url
|
53
|
-
|
54
|
-
'https://service.paygate.net/service/cancelAPI.json'
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'digest'
|
4
|
+
require 'uri'
|
5
|
+
require 'net/http'
|
6
|
+
|
7
|
+
module Paygate
|
8
|
+
class Transaction
|
9
|
+
FULL_AMOUNT_IDENTIFIER = 'F'
|
10
|
+
|
11
|
+
attr_reader :tid
|
12
|
+
attr_accessor :member
|
13
|
+
|
14
|
+
def initialize(tid)
|
15
|
+
@tid = tid
|
16
|
+
end
|
17
|
+
|
18
|
+
def refund(options = {})
|
19
|
+
# Encrypt data
|
20
|
+
api_key256 = ::Digest::SHA256.hexdigest(member.secret)
|
21
|
+
aes_ctr = AesCtr.encrypt(tid, api_key256, 256)
|
22
|
+
tid_enc = "AES256#{aes_ctr}"
|
23
|
+
|
24
|
+
# Prepare params
|
25
|
+
params = { callback: 'callback', mid: member.mid, tid: tid_enc }
|
26
|
+
params.merge!(options.slice(:amount))
|
27
|
+
params[:amount] ||= FULL_AMOUNT_IDENTIFIER
|
28
|
+
params[:mb_serial_no] = options[:order_id]
|
29
|
+
params.compact!
|
30
|
+
|
31
|
+
# Make request
|
32
|
+
uri = URI(self.class.refund_api_url)
|
33
|
+
uri.query = ::URI.encode_www_form(params)
|
34
|
+
response = ::Net::HTTP.get_response(uri)
|
35
|
+
|
36
|
+
r = Response.build_from_net_http_response(:refund, response)
|
37
|
+
r.raw_info = OpenStruct.new(tid: tid, tid_enc: tid_enc, request_url: uri.to_s) # rubocop:disable Style/OpenStructUse
|
38
|
+
r
|
39
|
+
end
|
40
|
+
|
41
|
+
# Doc: https://km.paygate.net/pages/viewpage.action?pageId=9207875
|
42
|
+
def verify
|
43
|
+
params = { tid: tid, verifyNum: 100 }
|
44
|
+
|
45
|
+
uri = URI('https://service.paygate.net/djemals/settle/verifyReceived.jsp')
|
46
|
+
uri.query = ::URI.encode_www_form(params)
|
47
|
+
response = ::Net::HTTP.get_response(uri)
|
48
|
+
|
49
|
+
Response.build_from_net_http_response(:verify, response)
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.refund_api_url
|
53
|
+
if Paygate.configuration.mode == :live
|
54
|
+
'https://service.paygate.net/service/cancelAPI.json'
|
55
|
+
else
|
56
|
+
'https://stgsvc.paygate.net/service/cancelAPI.json'
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/lib/paygate/version.rb
CHANGED