tbk 0.9.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.
- data/.gitignore +17 -0
- data/.travis.yml +8 -0
- data/.yardopts +5 -0
- data/Gemfile +8 -0
- data/LICENSE +20 -0
- data/README.md +101 -0
- data/Rakefile +6 -0
- data/lib/tbk/commerce.rb +53 -0
- data/lib/tbk/errors.rb +16 -0
- data/lib/tbk/keys/test_commerce.pem +27 -0
- data/lib/tbk/keys/webpay.101.pem +14 -0
- data/lib/tbk/keys/webpay_test.101.pem +14 -0
- data/lib/tbk/keys.rb +5 -0
- data/lib/tbk/version.rb +7 -0
- data/lib/tbk/webpay/confirmation.rb +111 -0
- data/lib/tbk/webpay/encryption.rb +71 -0
- data/lib/tbk/webpay/payment.rb +159 -0
- data/lib/tbk/webpay.rb +3 -0
- data/lib/tbk.rb +11 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/support/syntax.rb +5 -0
- data/spec/support/test_commerce.rb +12 -0
- data/spec/webpay/confirmation_spec.rb +4 -0
- data/spec/webpay/encryption_spec.rb +4 -0
- data/spec/webpay/payment_spec.rb +1 -0
- data/tbk.gemspec +25 -0
- metadata +164 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/.yardopts
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2013 Seba Gamboa
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
Pure Ruby implementation of Transbank's Webpay KCC 6.0 protocol
|
2
|
+
|
3
|
+
### Disclaimer
|
4
|
+
|
5
|
+
This library is not developed, supported nor endorsed in any way by Transbank S.A.
|
6
|
+
and is the result of reverse engineering Transbank's Integration Kit (aka. KCC)
|
7
|
+
for interoperability purposes as allowed by
|
8
|
+
[Chilean Law 20.435 Article 71 Ñ Section b](http://www.leychile.cl/Navegar?idNorma=1012827)
|
9
|
+
|
10
|
+
### Usage
|
11
|
+
|
12
|
+
Add this line to your application's Gemfile:
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
gem 'tbk'
|
16
|
+
```
|
17
|
+
|
18
|
+
To start a payment from your application
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
class WebpayController < ApplicationController
|
22
|
+
|
23
|
+
# ...
|
24
|
+
|
25
|
+
# Start a payment
|
26
|
+
def pay
|
27
|
+
# Initialyze you commerce
|
28
|
+
@commerce = TBK::Commerce.new({
|
29
|
+
id: YOUR_COMMERCE_ID,
|
30
|
+
key: YOUR_RSA_KEY
|
31
|
+
})
|
32
|
+
|
33
|
+
# Setup the payment
|
34
|
+
@payment = TBK::Webpay::Payment.new({
|
35
|
+
commerce: @commerce,
|
36
|
+
amount: ORDER_AMOUNT,
|
37
|
+
order_id: ORDER_ID,
|
38
|
+
success_url: webpay_success_url,
|
39
|
+
# Webpay can only access the HTTP protocol to a direct IP address (keep that in mind)
|
40
|
+
confirmation_url: webpay_confirmation_url(host: SERVER_IP_ADDRESS, protocol: 'http'),
|
41
|
+
|
42
|
+
# Optionaly supply:
|
43
|
+
session_id: SOME_SESSION_VALUE,
|
44
|
+
failure_url: webpay_failure_url # success_url is used by default
|
45
|
+
})
|
46
|
+
|
47
|
+
# Redirect the user to Webpay
|
48
|
+
redirect_to @payment.redirect_url
|
49
|
+
end
|
50
|
+
|
51
|
+
# ...
|
52
|
+
end
|
53
|
+
```
|
54
|
+
|
55
|
+
And to process a payment
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
class WebpayController < ApplicationController
|
59
|
+
|
60
|
+
# ...
|
61
|
+
|
62
|
+
# Confirmation callback executed from Webpay servers
|
63
|
+
def confirmation
|
64
|
+
# Initialyze you commerce
|
65
|
+
@commerce = TBK::Commerce.new({
|
66
|
+
id: YOUR_COMMERCE_ID,
|
67
|
+
key: YOUR_RSA_KEY
|
68
|
+
})
|
69
|
+
|
70
|
+
# Read the confirmation data from the request
|
71
|
+
@confirmation = TBK::Webpay::Confirmation.new({
|
72
|
+
commerce: @commerce,
|
73
|
+
post: request.raw_post
|
74
|
+
})
|
75
|
+
|
76
|
+
if # confirmation is invalid for some reason (wrong order_id or amount, double payment, etc...)
|
77
|
+
render text: @confirmation.reject
|
78
|
+
return # reject and stop execution
|
79
|
+
end
|
80
|
+
|
81
|
+
if @confirmation.success?
|
82
|
+
# EXITO!
|
83
|
+
# perform everything you have to do here.
|
84
|
+
end
|
85
|
+
|
86
|
+
# Acknowledge payment
|
87
|
+
render text: @confirmation.acknowledge
|
88
|
+
end
|
89
|
+
|
90
|
+
# ...
|
91
|
+
|
92
|
+
end
|
93
|
+
```
|
94
|
+
|
95
|
+
### Contributing
|
96
|
+
|
97
|
+
1. Fork it
|
98
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
99
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
100
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
101
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/lib/tbk/commerce.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
|
2
|
+
module TBK
|
3
|
+
# Represents a commerce registered with Transbank
|
4
|
+
class Commerce
|
5
|
+
|
6
|
+
# The registered commerce id
|
7
|
+
attr_accessor :id
|
8
|
+
|
9
|
+
# The commerce secret RSA key
|
10
|
+
attr_accessor :key
|
11
|
+
|
12
|
+
# Initialyzes a new commerce
|
13
|
+
# @param [Hash] attributes The commerce attributes
|
14
|
+
# @option attributes [Integer] :id The commerce ID
|
15
|
+
# @option attributes [String|OpenSSL::PKey::RSA] :key The commerce RSA private key
|
16
|
+
# @option attributes [Boolean] :test flag to set commerce in test mode
|
17
|
+
def initialize(attributes)
|
18
|
+
@test = attributes[:test]
|
19
|
+
|
20
|
+
self.id = attributes[:id]
|
21
|
+
raise TBK::CommerceError, "Missing commerce id" if self.id.nil?
|
22
|
+
|
23
|
+
self.key = case attributes[:key]
|
24
|
+
when String
|
25
|
+
OpenSSL::PKey::RSA.new(attributes[:key])
|
26
|
+
when OpenSSL::PKey::RSA.new
|
27
|
+
attributes[:key]
|
28
|
+
when nil
|
29
|
+
TEST_COMMERCE_KEY if self.test?
|
30
|
+
end
|
31
|
+
|
32
|
+
raise TBK::CommerceError, "Missing commerce key" if self.key.nil?
|
33
|
+
raise TBK::CommerceError, "Commerce key must be a RSA private key" unless self.key.private?
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [Boolean] wether or not the commerce is in test mode
|
37
|
+
def test?
|
38
|
+
@test || false
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [Boolean] wether or not the commerce is in production mode
|
42
|
+
def production?
|
43
|
+
!self.test?
|
44
|
+
end
|
45
|
+
|
46
|
+
# @return [Integer] RSA key bytes
|
47
|
+
def key_bytes
|
48
|
+
self.key.n.num_bytes
|
49
|
+
end
|
50
|
+
|
51
|
+
TEST_COMMERCE_KEY = TBK.parse_key('test_commerce')
|
52
|
+
end
|
53
|
+
end
|
data/lib/tbk/errors.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
module TBK
|
2
|
+
class Error < StandardError
|
3
|
+
attr_reader :origin
|
4
|
+
def initialize(msg, origin=nil)
|
5
|
+
super(msg)
|
6
|
+
@origin = origin
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class CommerceError < Error; end
|
11
|
+
|
12
|
+
module Webpay
|
13
|
+
class PaymentError < Error; end
|
14
|
+
class EncryptionError < Error; end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
-----BEGIN RSA PRIVATE KEY-----
|
2
|
+
MIIEowIBAAKCAQEAn3HzPC1ZBzCO3edUCf/XJiwj3bzJpjjTi/zBO9O+DDzZCaMp
|
3
|
+
14aspxQryvJhv8644E19Q+NHfxtz1cxd2wnSYKvay1gJx30ZlTOAkzUj4QMimR16
|
4
|
+
vomLlQ3T2MAz1znt/PVPVU7T/JOG9R+EbiHNVKa/hUjwJEFVXLQNME97nHoLjb3v
|
5
|
+
V5yV2aVhmox7b54n6F3UVPHvCsHKbJpXpE+vnLpVmdETbNpFVrDygXyG+mnEvyiO
|
6
|
+
BLIwEY3XTMrgXvS069groLi5Gg8C5LDaYOWjE9084T4fiWGrHhn2781R1rykunTu
|
7
|
+
77wiWPuQHMS0+YC7mhnsk8Z/ilD+aWz/vhsgHwIDAQABAoIBAQCM+Nrt4cpNKQmn
|
8
|
+
+Ne8348CGRS9ACXp6WRg6OCQXO4zM7lRZAminVgZgSQXE6aJR+T9rIWMeG7GWydX
|
9
|
+
aJGzEEQJZOjV0MkUr+7mk9qiTOGkGHmGlyHnRQU8jDU59vXe3UEl3l5+NmwHbQht
|
10
|
+
waf9F7XLmoLK/WoVJA6tICRpCl1oQrpziqN+gjdmMpz9i8I1sMFE7+Y7xf+7S2u7
|
11
|
+
c1MRPUWqgdS9yViQVh3vZi25m5CyKRVnOB0hpNuZ7nrJymtADYSWt9wV2W1fX+MX
|
12
|
+
UUoYfxyQQvWryHhGdedU7GGAnoEdblUcDkBuAaFmsm1P8K4HQZLWP4v6pYlW2JLa
|
13
|
+
Zoaerb3BAoGBANCRevl0CLB0HBU7sCs0eN9fTkIEsh3OVIxPSBqDnKsynJrIWovK
|
14
|
+
cs37Vb6phzdQO3ADoFJvR9ck8+v6Cv0KR8IOFl9wfC4ZoxkKBBeq94ZLN+YhE2PW
|
15
|
+
KiRFybqcgCtzxKS3MyWgpIcT9xFtHVjlorZ8Jk51fgLZbGzamtLhderVAoGBAMO0
|
16
|
+
mIiiV4l2vXzu4tFfkpu/GOx/D9/vAic3X9FOky09BNCyuMXMQgI8e3wWsGEZghls
|
17
|
+
Vg9KDV5EPxAmpumcdPFK2IMACaH41ac7vys3ZD8kMK0INQkuDAcG4YsxMaTwEPo0
|
18
|
+
p1i3zwwEWwknw1yJkOyozz0EcIzS9NrZZEjnBHEjAoGAQ81XdeqzvHEyg/CQd6sq
|
19
|
+
NCtubGXMZYYi1C4d2Yi5kKn2YRcK4HDi23V+TWodK+0oNWToZIQKjbVUmn0Bv3rt
|
20
|
+
EvezbDlMFUx+SfCIng0VRJIFTQmpnQYNUxdg2gpwXC/ZWFa6CNxtQABMjFy1cqXM
|
21
|
+
PJild1IYseJurgBu3mkvBTUCgYBqA/T1X2woLUis2wPIBAv5juXDh3lkB6eU8uxX
|
22
|
+
CEe2I+3t2EM781B2wajrKadWkmjluMhN9AGV5UZ8S1P0DStUYwUywdx1/8RNmZIP
|
23
|
+
qSwHAGXV9jI0zNr7G4Em0/leriWkRM26w6fHjLx8EyxDfsohSbkqBrOptcWqoEUx
|
24
|
+
MOQ5HQKBgAS4sbddOas2MapuhKU2surEb3Kz3RCIpta4bXgTQMt9wawcZSSpvnfT
|
25
|
+
zs5sehYvBFszL3MV98Uc50HXMf7gykRCmPRmB9S+f+kiVRvQDHfc9nRNg2XgcotU
|
26
|
+
KAE16PQM8GihQ0C+EcXHouyud5CRJGfyurokRlH/jY3BiRAG5c+6
|
27
|
+
-----END RSA PRIVATE KEY-----
|
@@ -0,0 +1,14 @@
|
|
1
|
+
-----BEGIN PUBLIC KEY-----
|
2
|
+
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxKKjroxE7X44TQovh9A9
|
3
|
+
ZpntP7LrdoyFsnJbDKjOOCoiid92FydN5qemyQCeXhsc7QHUXwGdth22fB8xJr3a
|
4
|
+
MZBEUJ+BKFrL+W6yE5V+F5Bj0Uq3lL0QMAIftGhLpgqw0ZMtU89kyd9Q4Rclq4r8
|
5
|
+
p2m/ZD7Pn5EmTOFSeyoWTMZQDl7OEoCKh/cZH5NJdWL08lCI+sGLOghRmFzkve4h
|
6
|
+
F9JCwKA7NYG7j3BWh39Oj2NIXEY/TO1Y3Y2WfNv9nvTpr46SpFlyp0KOhSiqgvXX
|
7
|
+
DgeXlebyqS82ch2DzOV9fjDAw7t71WXJBAev8Gd6HXwIXE/JP6AnLCa2Y+b6Wv8K
|
8
|
+
GWBCMIBXWL0m7WHeCaJ9Hx2yXZmHJh8FgeKffFKCwn3X90JiMocOSGsOE+Sfo85S
|
9
|
+
h/39Vc7vZS3i7kJDDoz9ab9/vFy30RuJf4p8Erh7kWtERVoG6/EhR+j4N3mgIOBZ
|
10
|
+
SHfzDAoOnqP5l7t2RXYcEbRLVN6o+XgUtalX33EJxJRsXoz9a6PxYlesIwPbKteD
|
11
|
+
BZ/xyJDwTc2gU2YzSH8G9anKrcvITBDULSAuxQUkYOiLbkb7vSKWDYKe0do6ibO3
|
12
|
+
RY/KXI63Q7bGKYaI2aa/8GnqVJ2G1U2s59NpqX0aaWjn59gsA8trA0YKOZP4xJIh
|
13
|
+
CvLM94G4V7lxe2IHKPqLscMCAwEAAQ==
|
14
|
+
-----END PUBLIC KEY-----
|
@@ -0,0 +1,14 @@
|
|
1
|
+
-----BEGIN PUBLIC KEY-----
|
2
|
+
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtKe3HHWwRcizAfkbS92V
|
3
|
+
fQr8cUb94TRjQPzNTqBduvvj65AD5J98Cn1htE3NzOz+PjPRcnfVe53V4f3+YlIb
|
4
|
+
6nnxyeuYLByiwoPkCmpOFBxNp04/Yh3dxN4xgOANXA37rNbDeO4WIEMG6zbdQMNJ
|
5
|
+
7RqQUlJSmui8gt3YxtqWBhBVW79qDCYVzxFrv3SH7pRuYEr+cxDvzRylxnJgr6ee
|
6
|
+
N7gmjoSMqF16f9aGdQ12obzV0A35BqpN6pRFoS/NvICbEeedS9g5gyUHf54a+juB
|
7
|
+
OV2HH5VJsCCgcb7I7Sio/xXTyP+QjIGJfpukkE8F+ohwRiChZ9jMXofPtuZYZiFQ
|
8
|
+
/gX08s5Qdpaph65UINP7crYbzpVJdrT2J0etyMcZbEanEkoX8YakLEBpPhyyR7mC
|
9
|
+
73fWd9sTuBEkG6kzCuG2JAyo6V8eyISnlKDEVd+/6G/Zpb5cUdBCERTYz5gvNoZN
|
10
|
+
zkuq4isiXh5MOLGs91H8ermuhdQe/lqvXf8Op/EYrAuxcdrZK0orI4LbPdUrC0Jc
|
11
|
+
Fl02qgXRrSpXo72anOlFc9P0blD4CMevW2+1wvIPA0DaJPsTnwBWOUqcfa7GAFH5
|
12
|
+
KGs3zCiZ5YTLDlnaps8koSssTVRi7LVT8HhiC5mjBklxmZjBv6ckgQeFWgp18kuU
|
13
|
+
ve5Elj5HSV7x2PCz8RKB4XcCAwEAAQ==
|
14
|
+
-----END PUBLIC KEY-----
|
data/lib/tbk/keys.rb
ADDED
data/lib/tbk/version.rb
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
module TBK
|
3
|
+
module Webpay
|
4
|
+
class Confirmation
|
5
|
+
RESPONSE_CODES = {
|
6
|
+
'0' => 'Transacción aprobada.',
|
7
|
+
'-1' => 'Rechazo de tx. en B24, No autorizada',
|
8
|
+
'-2' => 'Transacción debe reintentarse.',
|
9
|
+
'-3' => 'Error en tx.',
|
10
|
+
'-4' => 'Rechazo de tx. En B24, No autorizada',
|
11
|
+
'-5' => 'Rechazo por error de tasa.',
|
12
|
+
'-6' => 'Excede cupo máximo mensual.',
|
13
|
+
'-7' => 'Excede límite diario por transacción.',
|
14
|
+
'-8' => 'Rubro no autorizado.'
|
15
|
+
}
|
16
|
+
|
17
|
+
attr_accessor :commerce
|
18
|
+
attr_reader :raw
|
19
|
+
attr_reader :params
|
20
|
+
|
21
|
+
def initialize(options)
|
22
|
+
self.commerce = options[:commerce]
|
23
|
+
self.parse(options[:post])
|
24
|
+
end
|
25
|
+
|
26
|
+
def acknowledge
|
27
|
+
self.commerce.webpay_encrypt('ACK')
|
28
|
+
end
|
29
|
+
|
30
|
+
def reject
|
31
|
+
self.commerce.webpay_encrypt('ERR')
|
32
|
+
end
|
33
|
+
|
34
|
+
def success?
|
35
|
+
self.params["TBK_RESPUESTA"] == "0"
|
36
|
+
end
|
37
|
+
|
38
|
+
def order_id
|
39
|
+
self.params["TBK_ORDEN_COMPRA"]
|
40
|
+
end
|
41
|
+
|
42
|
+
def session_id
|
43
|
+
self.params["TBK_ID_SESION"]
|
44
|
+
end
|
45
|
+
|
46
|
+
def transaction_id
|
47
|
+
self.params["TBK_ID_TRANSACCION"]
|
48
|
+
end
|
49
|
+
|
50
|
+
def amount
|
51
|
+
self.params["TBK_MONTO"].to_f/100
|
52
|
+
end
|
53
|
+
|
54
|
+
def authorization
|
55
|
+
self.params["TBK_CODIGO_AUTORIZACION"]
|
56
|
+
end
|
57
|
+
|
58
|
+
def card_display_number
|
59
|
+
"XXXX-XXXX-XXXX-#{ card_last_numbers }"
|
60
|
+
end
|
61
|
+
|
62
|
+
def card_last_numbers
|
63
|
+
self.params["TBK_FINAL_NUMERO_TARJETA"]
|
64
|
+
end
|
65
|
+
|
66
|
+
def message
|
67
|
+
RESPONSE_CODES[self.params["TBK_RESPUESTA"]]
|
68
|
+
end
|
69
|
+
|
70
|
+
def paid_at
|
71
|
+
@paid_at ||= begin
|
72
|
+
year = Time.now.year
|
73
|
+
month = self.params["TBK_FECHA_TRANSACCION"][0...2].to_i
|
74
|
+
day = self.params["TBK_FECHA_TRANSACCION"][2...4].to_i
|
75
|
+
hour = self.params["TBK_HORA_TRANSACCION"][0...2].to_i
|
76
|
+
minutes = self.params["TBK_HORA_TRANSACCION"][2...4].to_i
|
77
|
+
seconds = self.params["TBK_HORA_TRANSACCION"][4...6].to_i
|
78
|
+
|
79
|
+
offset = if defined? TZInfo::Timezone
|
80
|
+
# Use tzinfo gem if available
|
81
|
+
TZInfo::Timezone.get('America/Santiago').period_for_utc(DateTime.new(year,month,day,hour,minutes,0)).utc_offset
|
82
|
+
else
|
83
|
+
-14400
|
84
|
+
end
|
85
|
+
|
86
|
+
Time.new(year, month, day, hour, minutes, seconds, offset)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
protected
|
92
|
+
def parse(post)
|
93
|
+
@raw = post.to_s
|
94
|
+
@raw_params = {}
|
95
|
+
for line in @raw.split('&')
|
96
|
+
key, value = *line.scan( %r{^([A-Za-z0-9_.]+)\=(.*)$} ).flatten
|
97
|
+
@raw_params[key] = CGI.unescape(value)
|
98
|
+
end
|
99
|
+
|
100
|
+
puts @raw_params.inspect
|
101
|
+
|
102
|
+
@params = {}
|
103
|
+
decrypted_params = self.commerce.webpay_decrypt(@raw_params['TBK_PARAM'])
|
104
|
+
for line in decrypted_params.split('#')
|
105
|
+
key, value = *line.scan( %r{^([A-Za-z0-9_.]+)\=(.*)$} ).flatten
|
106
|
+
@params[key] = CGI.unescape(value)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module TBK
|
2
|
+
module Webpay
|
3
|
+
module Encryption
|
4
|
+
# Constants
|
5
|
+
KEY_ID = 101
|
6
|
+
KEY = TBK.parse_key('webpay.101')
|
7
|
+
TEST_KEY = TBK.parse_key('webpay_test.101')
|
8
|
+
IV_PADDING = "\x10\xBB\xFF\xBF\x00\x00\x00\x00\x00\x00\x00\x00\xF4\xBF"
|
9
|
+
|
10
|
+
def webpay_key_id
|
11
|
+
KEY_ID
|
12
|
+
end
|
13
|
+
|
14
|
+
def webpay_key
|
15
|
+
self.production? ? KEY : TEST_KEY
|
16
|
+
end
|
17
|
+
|
18
|
+
def webpay_key_length
|
19
|
+
webpay_key.n.num_bytes
|
20
|
+
end
|
21
|
+
|
22
|
+
def webpay_encrypt(text)
|
23
|
+
signature = self.key.sign(OpenSSL::Digest::SHA512.new, text)
|
24
|
+
|
25
|
+
key = SecureRandom.random_bytes(32)
|
26
|
+
encripted_key = webpay_key.public_encrypt(key, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
|
27
|
+
|
28
|
+
iv = SecureRandom.random_bytes(16)
|
29
|
+
|
30
|
+
cipher = OpenSSL::Cipher.new('AES-256-CBC')
|
31
|
+
cipher.encrypt
|
32
|
+
cipher.key = key
|
33
|
+
cipher.iv = iv + IV_PADDING
|
34
|
+
encripted_text = cipher.update(signature + text) + cipher.final
|
35
|
+
|
36
|
+
Base64.encode64( iv + encripted_key + encripted_text).strip
|
37
|
+
rescue RuntimeError => error
|
38
|
+
raise TBK::Webpay::EncryptionError.new("Encryption failed",error)
|
39
|
+
end
|
40
|
+
|
41
|
+
def webpay_decrypt(encripted_text)
|
42
|
+
data = Base64.decode64(encripted_text)
|
43
|
+
|
44
|
+
iv = data[0...16]
|
45
|
+
encripted_key = data[16...(16 + self.key_bytes)]
|
46
|
+
key = self.key.private_decrypt(encripted_key, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
|
47
|
+
|
48
|
+
cipher = OpenSSL::Cipher.new('AES-256-CBC')
|
49
|
+
cipher.decrypt
|
50
|
+
cipher.key = key
|
51
|
+
cipher.iv = iv + IV_PADDING
|
52
|
+
decrypted_text = cipher.update(data[(16 + self.key_bytes)..-1]) + cipher.final
|
53
|
+
signature = decrypted_text[0...(webpay_key_length)]
|
54
|
+
text = decrypted_text[(webpay_key_length)..-1]
|
55
|
+
|
56
|
+
unless webpay_key.verify(OpenSSL::Digest::SHA512.new, signature, text)
|
57
|
+
raise TBK::Webpay::EncryptionError, "Invalid message signature"
|
58
|
+
end
|
59
|
+
|
60
|
+
text
|
61
|
+
rescue TBK::Error
|
62
|
+
raise
|
63
|
+
rescue => error
|
64
|
+
raise TBK::Webpay::EncryptionError.new("Decryption failed",error)
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
Commerce.send :include, Webpay::Encryption
|
71
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
module TBK
|
2
|
+
module Webpay
|
3
|
+
class Payment
|
4
|
+
attr_accessor :commerce
|
5
|
+
attr_accessor :amount
|
6
|
+
attr_accessor :order_id
|
7
|
+
attr_accessor :session_id
|
8
|
+
attr_accessor :confirmation_url
|
9
|
+
attr_accessor :success_url
|
10
|
+
attr_accessor :failure_url
|
11
|
+
|
12
|
+
def initialize(options = {})
|
13
|
+
self.commerce = options[:commerce]
|
14
|
+
self.amount = options[:amount]
|
15
|
+
self.order_id = options[:order_id]
|
16
|
+
self.session_id = options[:session_id]
|
17
|
+
self.confirmation_url = options[:confirmation_url]
|
18
|
+
self.success_url = options[:success_url]
|
19
|
+
self.failure_url = options[:failure_url]
|
20
|
+
end
|
21
|
+
|
22
|
+
# Transaction ids are generated randombly on the client side
|
23
|
+
def transaction_id
|
24
|
+
@transaction_id ||= (rand * 10000000000).to_i
|
25
|
+
end
|
26
|
+
|
27
|
+
def redirect_url
|
28
|
+
"#{ self.process_url }?TBK_VERSION_KCC=#{ TBK::VERSION::KCC }&TBK_TOKEN=#{ self.token }"
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_html_form(options = {}, &block)
|
32
|
+
form = <<-EOF
|
33
|
+
<form method="POST"
|
34
|
+
class="#{ options[:class] || 'tbk_payment_form'}"
|
35
|
+
id="#{ options[:id] }"
|
36
|
+
action="#{ self.process_url }">
|
37
|
+
<input type="hidden" name="TBK_PARAM" value="#{ self.param }">
|
38
|
+
<input type="hidden" name="TBK_VERSION_KCC" value="#{ TBK::VERSION::KCC }">
|
39
|
+
<input type="hidden" name="TBK_CODIGO_COMERCIO" value="#{ self.commerce.id }">
|
40
|
+
<input type="hidden" name="TBK_KEY_ID" value="#{ self.commerce.webpay_key_id }">
|
41
|
+
#{ yield if block_given? }
|
42
|
+
</form>
|
43
|
+
EOF
|
44
|
+
|
45
|
+
form = form.html_safe if form.respond_to? :html_safe
|
46
|
+
|
47
|
+
form
|
48
|
+
end
|
49
|
+
|
50
|
+
protected
|
51
|
+
def process_url
|
52
|
+
if self.commerce.test?
|
53
|
+
"https://certificacion.webpay.cl:6443/filtroUnificado/bp_revision.cgi"
|
54
|
+
else
|
55
|
+
"https://webpay.transbank.cl:443/cgi-bin/bp_revision.cgi"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def validation_url
|
60
|
+
if self.commerce.test?
|
61
|
+
"https://certificacion.webpay.cl:6443/filtroUnificado/bp_validacion.cgi"
|
62
|
+
else
|
63
|
+
"https://webpay.transbank.cl:443/cgi-bin/bp_validacion.cgi"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def token
|
68
|
+
@token ||= begin
|
69
|
+
uri = URI.parse( self.validation_url )
|
70
|
+
|
71
|
+
response = nil
|
72
|
+
until response && response['location'].nil?
|
73
|
+
uri = URI.parse( response.nil? ? self.validation_url : response['location'] )
|
74
|
+
|
75
|
+
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
|
76
|
+
post = Net::HTTP::Post.new uri.path
|
77
|
+
post["user-agent"] = "TBK/#{ TBK::VERSION::GEM } (Ruby/#{ RUBY_VERSION }; +#{ TBK::VERSION::WEBSITE })"
|
78
|
+
post.set_form_data({
|
79
|
+
'TBK_VERSION_KCC' => TBK::VERSION::KCC,
|
80
|
+
'TBK_CODIGO_COMERCIO' => self.commerce.id,
|
81
|
+
'TBK_KEY_ID' => self.commerce.webpay_key_id,
|
82
|
+
'TBK_PARAM' => self.param
|
83
|
+
})
|
84
|
+
|
85
|
+
# http.read_timeout = Webpay::Config.timeout
|
86
|
+
http.request post
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
unless response.code == "200"
|
91
|
+
raise TBK::Webpay::PaymentError, "Payment token generation failed"
|
92
|
+
end
|
93
|
+
|
94
|
+
response = self.commerce.webpay_decrypt(response.body)
|
95
|
+
|
96
|
+
unless /ERROR=([a-zA-Z0-9]+)/.match(response)[1] == "0"
|
97
|
+
raise TBK::Webpay::PaymentError, "Payment token generation failed"
|
98
|
+
end
|
99
|
+
|
100
|
+
/TOKEN=([a-zA-Z0-9]+)/.match(response)[1]
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def param
|
105
|
+
@param ||= begin
|
106
|
+
self.verify!
|
107
|
+
self.commerce.webpay_encrypt(raw_params)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def verify!
|
112
|
+
raise TBK::Webpay::PaymentError, "Commerce required" if self.commerce.nil?
|
113
|
+
raise TBK::Webpay::PaymentError, "Invalid amount (#{self.amount.inspect})" unless self.amount && self.amount > 0
|
114
|
+
raise TBK::Webpay::PaymentError, "Order ID required" if self.order_id.nil?
|
115
|
+
raise TBK::Webpay::PaymentError, "Success URL required" if self.success_url.nil?
|
116
|
+
raise TBK::Webpay::PaymentError, "Confirmation URL required" if self.confirmation_url.nil?
|
117
|
+
confirmation_uri = URI.parse(self.confirmation_url)
|
118
|
+
raise TBK::Webpay::PaymentError, "Confirmation URL host MUST be an IP address" unless /(\d{1,3}\.){3}\d{1,3}/.match(confirmation_uri.host)
|
119
|
+
raise TBK::Webpay::PaymentError, "Confirmation URL port MUST be 80 or 8080" unless [80, 8080].include?(confirmation_uri.port)
|
120
|
+
end
|
121
|
+
|
122
|
+
def raw_params(splitter="#", include_pseudomac=true)
|
123
|
+
params = []
|
124
|
+
|
125
|
+
params << "TBK_ORDEN_COMPRA=#{ self.order_id }"
|
126
|
+
params << "TBK_CODIGO_COMERCIO=#{ self.commerce.id }"
|
127
|
+
params << "TBK_ID_TRANSACCION=#{ self.transaction_id }"
|
128
|
+
|
129
|
+
uri = URI.parse(self.confirmation_url)
|
130
|
+
|
131
|
+
params << "TBK_URL_CGI_COMERCIO=#{ uri.path }"
|
132
|
+
params << "TBK_SERVIDOR_COMERCIO=#{ uri.host }"
|
133
|
+
params << "TBK_PUERTO_COMERCIO=#{ uri.port }"
|
134
|
+
|
135
|
+
params << "TBK_VERSION_KCC=#{ TBK::VERSION::KCC }"
|
136
|
+
params << "TBK_KEY_ID=#{ self.commerce.webpay_key_id }"
|
137
|
+
params << "PARAMVERIFCOM=1"
|
138
|
+
|
139
|
+
if include_pseudomac
|
140
|
+
# An old pseudo mac generation carried from an old implementation
|
141
|
+
digest = OpenSSL::Digest::MD5.new
|
142
|
+
digest << self.raw_params('&',false) << self.commerce.id.to_s << "webpay"
|
143
|
+
mac = digest.to_s
|
144
|
+
|
145
|
+
params << "TBK_MAC=#{ mac }"
|
146
|
+
end
|
147
|
+
|
148
|
+
|
149
|
+
params << "TBK_MONTO=#{ (self.amount * 100).to_i }"
|
150
|
+
params << "TBK_ID_SESION=#{ self.session_id }" if self.session_id
|
151
|
+
params << "TBK_URL_EXITO=#{ self.success_url }"
|
152
|
+
params << "TBK_URL_FRACASO=#{ self.failure_url || self.success_url }"
|
153
|
+
params << "TBK_TIPO_TRANSACCION=TR_NORMAL"
|
154
|
+
|
155
|
+
params.join(splitter)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
data/lib/tbk/webpay.rb
ADDED
data/lib/tbk.rb
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'spec_helper'
|
data/tbk.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'tbk/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "tbk"
|
8
|
+
gem.version = TBK::VERSION::GEM
|
9
|
+
gem.authors = ["Seba Gamboa"]
|
10
|
+
gem.email = ["me@sagmor.com"]
|
11
|
+
gem.description = %q{Ruby implementation of Transbank's Webpay protocol}
|
12
|
+
gem.summary = "Pure Ruby implementation of Transbank's Webpay KCC #{TBK::VERSION::KCC}"
|
13
|
+
gem.homepage = TBK::VERSION::WEBSITE
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
gem.add_development_dependency "tzinfo"
|
21
|
+
gem.add_development_dependency "rake"
|
22
|
+
gem.add_development_dependency "rspec"
|
23
|
+
gem.add_development_dependency "yard"
|
24
|
+
gem.add_development_dependency "redcarpet"
|
25
|
+
end
|
metadata
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tbk
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.9.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Seba Gamboa
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-01-25 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: tzinfo
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rake
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rspec
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: yard
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: redcarpet
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
description: Ruby implementation of Transbank's Webpay protocol
|
95
|
+
email:
|
96
|
+
- me@sagmor.com
|
97
|
+
executables: []
|
98
|
+
extensions: []
|
99
|
+
extra_rdoc_files: []
|
100
|
+
files:
|
101
|
+
- .gitignore
|
102
|
+
- .travis.yml
|
103
|
+
- .yardopts
|
104
|
+
- Gemfile
|
105
|
+
- LICENSE
|
106
|
+
- README.md
|
107
|
+
- Rakefile
|
108
|
+
- lib/tbk.rb
|
109
|
+
- lib/tbk/commerce.rb
|
110
|
+
- lib/tbk/errors.rb
|
111
|
+
- lib/tbk/keys.rb
|
112
|
+
- lib/tbk/keys/test_commerce.pem
|
113
|
+
- lib/tbk/keys/webpay.101.pem
|
114
|
+
- lib/tbk/keys/webpay_test.101.pem
|
115
|
+
- lib/tbk/version.rb
|
116
|
+
- lib/tbk/webpay.rb
|
117
|
+
- lib/tbk/webpay/confirmation.rb
|
118
|
+
- lib/tbk/webpay/encryption.rb
|
119
|
+
- lib/tbk/webpay/payment.rb
|
120
|
+
- spec/spec_helper.rb
|
121
|
+
- spec/support/syntax.rb
|
122
|
+
- spec/support/test_commerce.rb
|
123
|
+
- spec/webpay/confirmation_spec.rb
|
124
|
+
- spec/webpay/encryption_spec.rb
|
125
|
+
- spec/webpay/payment_spec.rb
|
126
|
+
- tbk.gemspec
|
127
|
+
homepage: http://sagmor.com/tbk/
|
128
|
+
licenses: []
|
129
|
+
post_install_message:
|
130
|
+
rdoc_options: []
|
131
|
+
require_paths:
|
132
|
+
- lib
|
133
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
134
|
+
none: false
|
135
|
+
requirements:
|
136
|
+
- - ! '>='
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
segments:
|
140
|
+
- 0
|
141
|
+
hash: -1481680372280631458
|
142
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
143
|
+
none: false
|
144
|
+
requirements:
|
145
|
+
- - ! '>='
|
146
|
+
- !ruby/object:Gem::Version
|
147
|
+
version: '0'
|
148
|
+
segments:
|
149
|
+
- 0
|
150
|
+
hash: -1481680372280631458
|
151
|
+
requirements: []
|
152
|
+
rubyforge_project:
|
153
|
+
rubygems_version: 1.8.23
|
154
|
+
signing_key:
|
155
|
+
specification_version: 3
|
156
|
+
summary: Pure Ruby implementation of Transbank's Webpay KCC 6.0
|
157
|
+
test_files:
|
158
|
+
- spec/spec_helper.rb
|
159
|
+
- spec/support/syntax.rb
|
160
|
+
- spec/support/test_commerce.rb
|
161
|
+
- spec/webpay/confirmation_spec.rb
|
162
|
+
- spec/webpay/encryption_spec.rb
|
163
|
+
- spec/webpay/payment_spec.rb
|
164
|
+
has_rdoc:
|