paygate-ruby 0.1.11 → 0.2.1
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 +12 -0
- data/Rakefile +2 -2
- data/data/cdbn.20230401.unl.xlsx +0 -0
- data/data/config.yml +3337 -2734
- data/lib/paygate/action_view/form_helper.rb +159 -0
- data/lib/paygate/aes.rb +37 -35
- data/lib/paygate/aes_ctr.rb +58 -55
- data/lib/paygate/configuration.rb +7 -3
- data/lib/paygate/member.rb +4 -1
- data/lib/paygate/profile.rb +5 -2
- data/lib/paygate/response.rb +10 -8
- data/lib/paygate/transaction.rb +11 -9
- data/lib/paygate/version.rb +3 -1
- data/lib/paygate-ruby.rb +2 -47
- data/lib/paygate.rb +46 -0
- metadata +11 -55
- data/data/card_bin_20191001.xlsx +0 -0
- data/lib/paygate/helpers/form_helper.rb +0 -155
- data/spec/integration/paygate_spec.rb +0 -9
- data/spec/spec_helper.rb +0 -10
@@ -0,0 +1,159 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Paygate
|
4
|
+
module ActionView
|
5
|
+
module FormHelper
|
6
|
+
PAYGATE_FORM_TEXT_FIELDS = {
|
7
|
+
mid: {
|
8
|
+
placeholder: 'Member ID'
|
9
|
+
},
|
10
|
+
|
11
|
+
locale: {
|
12
|
+
name: 'langcode',
|
13
|
+
default: 'KR',
|
14
|
+
placeholder: 'Language'
|
15
|
+
},
|
16
|
+
|
17
|
+
charset: {
|
18
|
+
default: 'UTF-8',
|
19
|
+
placeholder: 'Charset'
|
20
|
+
},
|
21
|
+
|
22
|
+
title: {
|
23
|
+
name: 'goodname',
|
24
|
+
placeholder: 'Title'
|
25
|
+
},
|
26
|
+
|
27
|
+
currency: {
|
28
|
+
name: 'goodcurrency',
|
29
|
+
default: 'WON',
|
30
|
+
placeholder: 'Currency'
|
31
|
+
},
|
32
|
+
|
33
|
+
amount: {
|
34
|
+
name: 'unitprice',
|
35
|
+
placeholder: 'Amount'
|
36
|
+
},
|
37
|
+
|
38
|
+
meta1: {
|
39
|
+
name: 'goodoption1',
|
40
|
+
placeholder: 'Good Option 1'
|
41
|
+
},
|
42
|
+
|
43
|
+
meta2: {
|
44
|
+
name: 'goodoption2',
|
45
|
+
placeholder: 'Good Option 2'
|
46
|
+
},
|
47
|
+
|
48
|
+
meta3: {
|
49
|
+
name: 'goodoption3',
|
50
|
+
placeholder: 'Good Option 3'
|
51
|
+
},
|
52
|
+
|
53
|
+
meta4: {
|
54
|
+
name: 'goodoption4',
|
55
|
+
placeholder: 'Good Option 4'
|
56
|
+
},
|
57
|
+
|
58
|
+
meta5: {
|
59
|
+
name: 'goodoption5',
|
60
|
+
placeholder: 'Good Option 5'
|
61
|
+
},
|
62
|
+
|
63
|
+
pay_method: {
|
64
|
+
name: 'paymethod',
|
65
|
+
default: 'card',
|
66
|
+
placeholder: 'Pay Method'
|
67
|
+
},
|
68
|
+
|
69
|
+
customer_name: {
|
70
|
+
name: 'receipttoname',
|
71
|
+
placeholder: 'Customer Name'
|
72
|
+
},
|
73
|
+
|
74
|
+
customer_email: {
|
75
|
+
name: 'receipttoemail',
|
76
|
+
placeholder: 'Customer Email'
|
77
|
+
},
|
78
|
+
|
79
|
+
card_number: {
|
80
|
+
name: 'cardnumber',
|
81
|
+
placeholder: 'Card Number'
|
82
|
+
},
|
83
|
+
|
84
|
+
expiry_year: {
|
85
|
+
name: 'cardexpireyear',
|
86
|
+
placeholder: 'Expiry Year'
|
87
|
+
},
|
88
|
+
|
89
|
+
expiry_month: {
|
90
|
+
name: 'cardexpiremonth',
|
91
|
+
placeholder: 'Expiry Month'
|
92
|
+
},
|
93
|
+
|
94
|
+
cvv: {
|
95
|
+
name: 'cardsecretnumber',
|
96
|
+
placeholder: 'CVV'
|
97
|
+
},
|
98
|
+
|
99
|
+
card_auth_code: {
|
100
|
+
name: 'cardauthcode',
|
101
|
+
placeholder: 'Card Auth Code'
|
102
|
+
},
|
103
|
+
|
104
|
+
response_code: {
|
105
|
+
name: 'replycode',
|
106
|
+
placeholder: 'Response Code'
|
107
|
+
},
|
108
|
+
|
109
|
+
response_message: {
|
110
|
+
name: 'replyMsg',
|
111
|
+
placeholder: 'Response Message'
|
112
|
+
},
|
113
|
+
|
114
|
+
tid: {
|
115
|
+
placeholder: 'TID'
|
116
|
+
},
|
117
|
+
|
118
|
+
profile_no: {
|
119
|
+
placeholder: 'Profile No'
|
120
|
+
},
|
121
|
+
|
122
|
+
hash_result: {
|
123
|
+
name: 'hashresult',
|
124
|
+
placeholder: 'Hash Result'
|
125
|
+
}
|
126
|
+
}.freeze
|
127
|
+
|
128
|
+
def paygate_open_pay_api_js_url
|
129
|
+
if Paygate.configuration.mode == :live
|
130
|
+
'https://api.paygate.net/ajax/common/OpenPayAPI.js'
|
131
|
+
else
|
132
|
+
'https://stgapi.paygate.net/ajax/common/OpenPayAPI.js'
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def paygate_open_pay_api_form(options = {})
|
137
|
+
form_tag({}, name: 'PGIOForm') do
|
138
|
+
fields = []
|
139
|
+
|
140
|
+
PAYGATE_FORM_TEXT_FIELDS.each do |key, opts|
|
141
|
+
arg_opts = options[key] || {}
|
142
|
+
fields << text_field_tag(
|
143
|
+
key,
|
144
|
+
arg_opts[:value] || opts[:default],
|
145
|
+
name: opts[:name] || key.to_s,
|
146
|
+
placeholder: arg_opts[:placeholder] || opts[:placeholder]
|
147
|
+
).html_safe
|
148
|
+
end
|
149
|
+
|
150
|
+
fields.join.html_safe
|
151
|
+
end.html_safe
|
152
|
+
end
|
153
|
+
|
154
|
+
def paygate_open_pay_api_screen
|
155
|
+
content_tag(:div, nil, id: 'PGIOscreen')
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
data/lib/paygate/aes.rb
CHANGED
@@ -1,22 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Paygate
|
2
4
|
class Aes
|
3
5
|
# Pre-computed multiplicative inverse in GF(2^8)
|
4
|
-
S_BOX = [0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,
|
5
|
-
0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,
|
6
|
-
0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,
|
7
|
-
0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,
|
8
|
-
0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,
|
9
|
-
0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,
|
10
|
-
0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,
|
11
|
-
0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,
|
12
|
-
0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,
|
13
|
-
0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,
|
14
|
-
0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,
|
15
|
-
0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,
|
16
|
-
0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,
|
17
|
-
0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,
|
18
|
-
0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,
|
19
|
-
0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16].freeze
|
6
|
+
S_BOX = [0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76,
|
7
|
+
0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0,
|
8
|
+
0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15,
|
9
|
+
0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75,
|
10
|
+
0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84,
|
11
|
+
0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf,
|
12
|
+
0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8,
|
13
|
+
0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2,
|
14
|
+
0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73,
|
15
|
+
0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb,
|
16
|
+
0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79,
|
17
|
+
0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08,
|
18
|
+
0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a,
|
19
|
+
0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e,
|
20
|
+
0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf,
|
21
|
+
0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16].freeze
|
20
22
|
|
21
23
|
# Round Constant used for the Key Expansion [1st col is 2^(r-1) in GF(2^8)]
|
22
24
|
R_CON = [[0x00, 0x00, 0x00, 0x00],
|
@@ -29,10 +31,10 @@ module Paygate
|
|
29
31
|
[0x40, 0x00, 0x00, 0x00],
|
30
32
|
[0x80, 0x00, 0x00, 0x00],
|
31
33
|
[0x1b, 0x00, 0x00, 0x00],
|
32
|
-
[0x36, 0x00, 0x00, 0x00]
|
34
|
+
[0x36, 0x00, 0x00, 0x00]].freeze
|
33
35
|
|
34
36
|
# Block size (in words): no of columns in state (fixed for AES)
|
35
|
-
CIPHER_BLOCK_SIZE = 4
|
37
|
+
CIPHER_BLOCK_SIZE = 4
|
36
38
|
|
37
39
|
# AES Cipher function: encrypt 'input' state with Rijndael algorithm
|
38
40
|
# applies Nr rounds (10/12/14) using key schedule w for 'add round key' stage
|
@@ -40,8 +42,8 @@ module Paygate
|
|
40
42
|
# @param int[][] w Key schedule as 2D byte-array (Nr+1 x Nb bytes)
|
41
43
|
# @returns int[] Encrypted output state array
|
42
44
|
def self.cipher(input, w)
|
43
|
-
nr = w.length / CIPHER_BLOCK_SIZE - 1
|
44
|
-
state = [[],[],[],[]]
|
45
|
+
nr = (w.length / CIPHER_BLOCK_SIZE) - 1 # no of rounds: 10/12/14 for 128/192/256-bit keys
|
46
|
+
state = [[], [], [], []] # initialize 4x4 byte-array 'state' with input
|
45
47
|
|
46
48
|
(0...(4 * CIPHER_BLOCK_SIZE)).each do |i|
|
47
49
|
state[i % 4][(i / 4.0).floor] = input[i]
|
@@ -74,17 +76,17 @@ module Paygate
|
|
74
76
|
|
75
77
|
w = []
|
76
78
|
temp = []
|
77
|
-
0.upto(nk - 1){ |i| w[i] = [key[4 * i], key[4 * i + 1], key[4 * i + 2], key[4 * i + 3]] }
|
79
|
+
0.upto(nk - 1) { |i| w[i] = [key[4 * i], key[(4 * i) + 1], key[(4 * i) + 2], key[(4 * i) + 3]] }
|
78
80
|
(nk...(CIPHER_BLOCK_SIZE * (nr + 1))).each do |i|
|
79
81
|
w[i] = []
|
80
|
-
0.upto(3){ |t| temp[t] = w[i - 1][t] }
|
81
|
-
if i % nk
|
82
|
+
0.upto(3) { |t| temp[t] = w[i - 1][t] }
|
83
|
+
if (i % nk).zero?
|
82
84
|
temp = sub_word(rotate_word(temp))
|
83
|
-
0.upto(3){ |t| temp[t] ^= R_CON[i / nk][t]}
|
85
|
+
0.upto(3) { |t| temp[t] ^= R_CON[i / nk][t] }
|
84
86
|
elsif nk > 6 && i % nk == 4
|
85
87
|
temp = sub_word(temp)
|
86
88
|
end
|
87
|
-
0.upto(3){ |t| w[i][t] = w[i - nk][t] ^ temp[t] }
|
89
|
+
0.upto(3) { |t| w[i][t] = w[i - nk][t] ^ temp[t] }
|
88
90
|
end
|
89
91
|
w
|
90
92
|
end
|
@@ -92,7 +94,7 @@ module Paygate
|
|
92
94
|
# apply SBox to state
|
93
95
|
def self.sub_bytes(state)
|
94
96
|
0.upto(3) do |r|
|
95
|
-
0.upto(CIPHER_BLOCK_SIZE - 1){ |c| state[r][c] = S_BOX[state[r][c]] }
|
97
|
+
0.upto(CIPHER_BLOCK_SIZE - 1) { |c| state[r][c] = S_BOX[state[r][c]] }
|
96
98
|
end
|
97
99
|
state
|
98
100
|
end
|
@@ -101,20 +103,20 @@ module Paygate
|
|
101
103
|
def self.shift_rows(state)
|
102
104
|
t = []
|
103
105
|
1.upto(3) do |r|
|
104
|
-
0.upto(3){ |c| t[c] = state[r][(c + r) % CIPHER_BLOCK_SIZE] } # shift into temp copy
|
105
|
-
0.upto(3){ |c| state[r][c] = t[c] } # and copy back
|
106
|
-
end
|
106
|
+
0.upto(3) { |c| t[c] = state[r][(c + r) % CIPHER_BLOCK_SIZE] } # shift into temp copy
|
107
|
+
0.upto(3) { |c| state[r][c] = t[c] } # and copy back
|
108
|
+
end
|
107
109
|
state # see asmaes.sourceforge.net/rijndael/rijndaelImplementation.pdf
|
108
110
|
end
|
109
111
|
|
110
112
|
# combine bytes of each col of state S
|
111
113
|
def self.mix_columns(state)
|
112
114
|
0.upto(3) do |c|
|
113
|
-
a=[] # 'a' is a copy of the current column from 'state'
|
114
|
-
b=[] # 'b' is a•{02} in GF(2^8)
|
115
|
+
a = [] # 'a' is a copy of the current column from 'state'
|
116
|
+
b = [] # 'b' is a•{02} in GF(2^8)
|
115
117
|
0.upto(3) do |i|
|
116
118
|
a[i] = state[i][c]
|
117
|
-
b[i] = (state[i][c] & 0x80
|
119
|
+
b[i] = (state[i][c] & 0x80).zero? ? (state[i][c] << 1) ^ 0x011b : state[i][c] << 1
|
118
120
|
end
|
119
121
|
# a[n] ^ b[n] is a•{03} in GF(2^8)
|
120
122
|
state[0][c] = b[0] ^ a[1] ^ b[1] ^ a[2] ^ a[3] # 2*a0 + 3*a1 + a2 + a3
|
@@ -128,21 +130,21 @@ module Paygate
|
|
128
130
|
# xor Round Key into state S
|
129
131
|
def self.add_round_key(state, w, rnd)
|
130
132
|
0.upto(3) do |r|
|
131
|
-
0.upto(CIPHER_BLOCK_SIZE - 1){ |c| state[r][c] ^= w[rnd * 4 + c][r]}
|
133
|
+
0.upto(CIPHER_BLOCK_SIZE - 1) { |c| state[r][c] ^= w[(rnd * 4) + c][r] }
|
132
134
|
end
|
133
135
|
state
|
134
136
|
end
|
135
137
|
|
136
138
|
# apply SBox to 4-byte word w
|
137
139
|
def self.sub_word(word)
|
138
|
-
0.upto(3){ |i| word[i] = S_BOX[word[i]] }
|
140
|
+
0.upto(3) { |i| word[i] = S_BOX[word[i]] }
|
139
141
|
word
|
140
142
|
end
|
141
143
|
|
142
144
|
# rotate 4-byte word w left by one byte
|
143
145
|
def self.rotate_word(word)
|
144
146
|
tmp = word[0]
|
145
|
-
0.upto(2){ |i| word[i] = word[i + 1] }
|
147
|
+
0.upto(2) { |i| word[i] = word[i + 1] }
|
146
148
|
word[3] = tmp
|
147
149
|
word
|
148
150
|
end
|
data/lib/paygate/aes_ctr.rb
CHANGED
@@ -1,8 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'base64'
|
2
4
|
|
3
5
|
module Paygate
|
4
6
|
class AesCtr
|
5
|
-
|
6
7
|
# Encrypt a text using AES encryption in Counter mode of operation
|
7
8
|
#
|
8
9
|
# Unicode multi-byte character safe
|
@@ -12,16 +13,19 @@ module Paygate
|
|
12
13
|
# @param int num_bits Number of bits to be used in the key (128, 192, or 256)
|
13
14
|
# @returns string Encrypted text
|
14
15
|
def self.encrypt(plaintext, password, num_bits)
|
15
|
-
block_size = 16
|
16
|
-
return '' unless
|
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)
|
17
18
|
|
18
19
|
# use AES itself to encrypt password to get cipher key (using plain password as source for key
|
19
20
|
# expansion) - gives us well encrypted key (though hashed key might be preferred for prod'n use)
|
20
|
-
num_bytes = num_bits / 8
|
21
|
+
num_bytes = num_bits / 8 # no bytes in key (16/24/32)
|
21
22
|
pw_bytes = []
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
25
29
|
|
26
30
|
# initialise 1st 8 bytes of counter block with nonce (NIST SP800-38A §B.2): [0-1] = millisec,
|
27
31
|
# [2-3] = random, [4-7] = seconds, together giving full sub-millisec uniqueness up to Feb 2106
|
@@ -29,14 +33,14 @@ module Paygate
|
|
29
33
|
nonce = Time.now.to_i
|
30
34
|
nonce_ms = nonce % 1000
|
31
35
|
nonce_sec = (nonce / 1000.0).floor
|
32
|
-
nonce_rand = (rand
|
33
|
-
0.upto(1){ |i| counter_block[i] = urs(nonce_ms, i * 8) & 0xff }
|
34
|
-
0.upto(1){ |i| counter_block[i + 2] = urs(nonce_rand, i * 8) & 0xff }
|
35
|
-
0.upto(3){ |i| counter_block[i + 4] = urs(nonce_sec, i * 8) & 0xff }
|
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 }
|
36
40
|
|
37
41
|
# and convert it to a string to go on the front of the ciphertext
|
38
42
|
ctr_text = ''
|
39
|
-
0.upto(7){ |i| ctr_text += counter_block[i].chr }
|
43
|
+
0.upto(7) { |i| ctr_text += counter_block[i].chr }
|
40
44
|
|
41
45
|
# generate key schedule - an expansion of the key into distinct Key Rounds for each round
|
42
46
|
key_schedule = Aes.key_expansion(key)
|
@@ -46,85 +50,84 @@ module Paygate
|
|
46
50
|
0.upto(block_count - 1) do |b|
|
47
51
|
# set counter (block #) in last 8 bytes of counter block (leaving nonce in 1st 8 bytes)
|
48
52
|
# done in two stages for 32-bit ops: using two words allows us to go past 2^32 blocks (68GB)
|
49
|
-
0.upto(3){ |c| counter_block[15 - c] = urs(b, c * 8) & 0xff }
|
50
|
-
0.upto(3){ |c| counter_block[15 - c - 4] = urs(b / 0x100000000, c * 8) }
|
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) }
|
51
55
|
|
52
56
|
cipher_cntr = Aes.cipher(counter_block, key_schedule) # -- encrypt counter block --
|
53
57
|
# block size is reduced on final block
|
54
|
-
block_length =
|
58
|
+
block_length = b < block_count - 1 ? block_size : ((plaintext.length - 1) % block_size) + 1
|
55
59
|
cipher_char = []
|
56
60
|
0.upto(block_length - 1) do |i|
|
57
|
-
cipher_char[i] = (cipher_cntr[i] ^ plaintext.bytes.to_a[b * block_size + i]).chr
|
61
|
+
cipher_char[i] = (cipher_cntr[i] ^ plaintext.bytes.to_a[(b * block_size) + i]).chr
|
58
62
|
end
|
59
63
|
cipher_text[b] = cipher_char.join
|
60
64
|
end
|
61
65
|
|
62
66
|
cipher_text = ctr_text + cipher_text.join
|
63
|
-
|
67
|
+
"#{Base64.encode64(cipher_text).delete("\n")}\n" # encode in base64
|
64
68
|
end
|
65
69
|
|
66
70
|
# Decrypt a text encrypted by AES in counter mode of operation
|
67
71
|
#
|
68
72
|
# @param string ciphertext Source text to be encrypted
|
69
73
|
# @param string password The password to use to generate a key
|
70
|
-
# @param int
|
74
|
+
# @param int n_bits Number of bits to be used in the key (128, 192, or 256)
|
71
75
|
# @returns string
|
72
76
|
# Decrypted text
|
73
|
-
def self.decrypt(ciphertext, password,
|
74
|
-
|
75
|
-
return '' unless
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
key =
|
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
|
83
88
|
# recover nonce from 1st 8 bytes of ciphertext
|
84
|
-
|
85
|
-
|
86
|
-
0.upto(7){|i|
|
89
|
+
counter_block = []
|
90
|
+
ctr_txt = ciphertext[0, 8]
|
91
|
+
0.upto(7) { |i| counter_block[i] = ctr_txt.bytes.to_a[i] }
|
87
92
|
|
88
|
-
#generate key Schedule
|
89
|
-
|
93
|
+
# generate key Schedule
|
94
|
+
key_schedule = Aes.key_expansion(key)
|
90
95
|
|
91
96
|
# separate ciphertext into blocks (skipping past initial 8 bytes)
|
92
|
-
|
93
|
-
ct=[]
|
94
|
-
0.upto(
|
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] }
|
95
100
|
|
96
|
-
ciphertext = ct;
|
101
|
+
ciphertext = ct; # ciphertext is now array of block-length strings
|
97
102
|
|
98
103
|
# plaintext will get generated block-by-block into array of block-length strings
|
99
|
-
plaintxt = []
|
100
|
-
0.upto(
|
101
|
-
0.upto(3){|c|
|
102
|
-
0.upto(3){|c|
|
103
|
-
|
104
|
-
|
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 = []
|
105
110
|
0.upto(ciphertext[b].length - 1) do |i|
|
106
111
|
# -- xor plaintxt with ciphered counter byte-by-byte --
|
107
|
-
|
112
|
+
plaintxt_byte[i] = (cipher_cntr[i] ^ ciphertext[b].bytes.to_a[i]).chr
|
108
113
|
end
|
109
|
-
plaintxt[b] =
|
114
|
+
plaintxt[b] = plaintxt_byte.join
|
110
115
|
end
|
111
|
-
|
116
|
+
plaintxt.join
|
112
117
|
end
|
113
118
|
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
# @param b number of bits to shift a to the right (0..31)
|
120
|
-
# @return a right-shifted and zero-filled by b bits
|
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
|
121
124
|
def self.urs(a, b)
|
122
125
|
a &= 0xffffffff
|
123
126
|
b &= 0x1f
|
124
|
-
if a & 0x80000000 && b
|
127
|
+
if (a & 0x80000000) && b.positive? # if left-most bit set
|
125
128
|
a = ((a >> 1) & 0x7fffffff) # right-shift one bit & clear left-most bit
|
126
129
|
a = a >> (b - 1) # remaining right-shifts
|
127
|
-
else
|
130
|
+
else # otherwise
|
128
131
|
a = (a >> b); # use normal right-shift
|
129
132
|
end
|
130
133
|
a
|
@@ -1,6 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Paygate
|
2
4
|
class Configuration
|
3
|
-
MODES = %i
|
5
|
+
MODES = %i[live sandbox].freeze
|
4
6
|
|
5
7
|
attr_reader :mode
|
6
8
|
|
@@ -9,8 +11,10 @@ module Paygate
|
|
9
11
|
end
|
10
12
|
|
11
13
|
def mode=(value)
|
12
|
-
|
13
|
-
|
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
|
14
18
|
end
|
15
19
|
end
|
16
20
|
end
|
data/lib/paygate/member.rb
CHANGED
data/lib/paygate/profile.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'uri'
|
2
4
|
require 'net/http'
|
3
5
|
|
4
6
|
module Paygate
|
5
7
|
class Profile
|
6
|
-
PURCHASE_URL = 'https://service.paygate.net/INTL/pgtlProcess3.jsp'
|
8
|
+
PURCHASE_URL = 'https://service.paygate.net/INTL/pgtlProcess3.jsp'
|
7
9
|
|
8
10
|
attr_reader :profile_no
|
9
11
|
attr_accessor :member
|
@@ -17,7 +19,8 @@ module Paygate
|
|
17
19
|
params = { profile_no: profile_no,
|
18
20
|
mid: member.mid,
|
19
21
|
goodcurrency: currency,
|
20
|
-
unitprice: amount }
|
22
|
+
unitprice: amount }
|
23
|
+
params.compact!
|
21
24
|
|
22
25
|
# Make request
|
23
26
|
uri = URI(PURCHASE_URL)
|
data/lib/paygate/response.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Paygate
|
2
4
|
class Response
|
3
5
|
attr_accessor :transaction_type, :http_code, :message, :body, :raw_info, :json
|
@@ -10,14 +12,14 @@ module Paygate
|
|
10
12
|
r.body = response.body
|
11
13
|
|
12
14
|
case txn_type
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
21
23
|
end
|
22
24
|
r
|
23
25
|
end
|
data/lib/paygate/transaction.rb
CHANGED
@@ -1,10 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'digest'
|
2
4
|
require 'uri'
|
3
5
|
require 'net/http'
|
4
6
|
|
5
7
|
module Paygate
|
6
8
|
class Transaction
|
7
|
-
FULL_AMOUNT_IDENTIFIER = 'F'
|
9
|
+
FULL_AMOUNT_IDENTIFIER = 'F'
|
8
10
|
|
9
11
|
attr_reader :tid
|
10
12
|
attr_accessor :member
|
@@ -15,8 +17,8 @@ module Paygate
|
|
15
17
|
|
16
18
|
def refund(options = {})
|
17
19
|
# Encrypt data
|
18
|
-
|
19
|
-
aes_ctr = AesCtr.encrypt(tid,
|
20
|
+
api_key256 = ::Digest::SHA256.hexdigest(member.secret)
|
21
|
+
aes_ctr = AesCtr.encrypt(tid, api_key256, 256)
|
20
22
|
tid_enc = "AES256#{aes_ctr}"
|
21
23
|
|
22
24
|
# Prepare params
|
@@ -24,7 +26,7 @@ module Paygate
|
|
24
26
|
params.merge!(options.slice(:amount))
|
25
27
|
params[:amount] ||= FULL_AMOUNT_IDENTIFIER
|
26
28
|
params[:mb_serial_no] = options[:order_id]
|
27
|
-
params.
|
29
|
+
params.compact!
|
28
30
|
|
29
31
|
# Make request
|
30
32
|
uri = URI(self.class.refund_api_url)
|
@@ -32,7 +34,7 @@ module Paygate
|
|
32
34
|
response = ::Net::HTTP.get_response(uri)
|
33
35
|
|
34
36
|
r = Response.build_from_net_http_response(:refund, response)
|
35
|
-
r.raw_info = OpenStruct.new(tid: tid, tid_enc: tid_enc, request_url: uri.to_s)
|
37
|
+
r.raw_info = OpenStruct.new(tid: tid, tid_enc: tid_enc, request_url: uri.to_s) # rubocop:disable Style/OpenStructUse
|
36
38
|
r
|
37
39
|
end
|
38
40
|
|
@@ -47,12 +49,12 @@ module Paygate
|
|
47
49
|
Response.build_from_net_http_response(:verify, response)
|
48
50
|
end
|
49
51
|
|
50
|
-
private
|
51
|
-
|
52
52
|
def self.refund_api_url
|
53
|
-
|
54
|
-
'https://service.paygate.net/service/cancelAPI.json'
|
53
|
+
if Paygate.configuration.mode == :live
|
54
|
+
'https://service.paygate.net/service/cancelAPI.json'
|
55
|
+
else
|
55
56
|
'https://stgsvc.paygate.net/service/cancelAPI.json'
|
57
|
+
end
|
56
58
|
end
|
57
59
|
end
|
58
60
|
end
|
data/lib/paygate/version.rb
CHANGED