redsys-ruby 0.1.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 +7 -0
- data/.jules/palette.md +7 -0
- data/.jules/sentinel.md +24 -0
- data/README.md +116 -0
- data/Rakefile +8 -0
- data/app/controllers/redsys_ruby/application_controller.rb +7 -0
- data/app/controllers/redsys_ruby/configurations_controller.rb +32 -0
- data/app/controllers/redsys_ruby/payments_controller.rb +16 -0
- data/app/helpers/redsys_ruby/payments_helper.rb +39 -0
- data/app/models/redsys_ruby/configuration.rb +63 -0
- data/app/views/redsys_ruby/configurations/edit.html.erb +152 -0
- data/app/views/redsys_ruby/payments/index.html.erb +77 -0
- data/app/views/redsys_ruby/payments/ko.html.erb +124 -0
- data/app/views/redsys_ruby/payments/ok.html.erb +107 -0
- data/config/routes.rb +12 -0
- data/lib/redsys-ruby/engine.rb +7 -0
- data/lib/redsys-ruby/tpv.rb +104 -0
- data/lib/redsys-ruby/version.rb +5 -0
- data/lib/redsys-ruby.rb +23 -0
- data/sig/redsys_ruby.rbs +4 -0
- metadata +77 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f2c59de6ca071087c65956f103290027fc275a3a91c95e09c0fda848880277d3
|
|
4
|
+
data.tar.gz: ac727ac835a38599f63a17f5e5914cda495e7eae09a8235af5fc0201690aed45
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 528545461443b6f03d37ebe624fd96157eb5c609f505fa96f03d33f4e4cdd6bf7d3c05b174c82e9202b48cf6c72f0c05d4c6890e8a937542b44b1afad675ecf5
|
|
7
|
+
data.tar.gz: 79b53403ef78056c8905a6998d211369358422bacad3e3215c2f952436d8f80d794c869129c22c2c537258e11a2b691979d6cd9b83082e7f5079a4a37a52c48a
|
data/.jules/palette.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
## 2025-05-15 - Redsys Payment Trust Markers
|
|
2
|
+
**Learning:** Adding trust markers like "Secure Payment" notices and lock icons is crucial for UX in financial transactions, even in demo or redirect pages. It reduces user anxiety before being redirected to an external gateway.
|
|
3
|
+
**Action:** Always include trust signals (lock icons, security text) near payment-related CTA buttons.
|
|
4
|
+
|
|
5
|
+
## 2025-05-15 - Immediate Feedback on Redirect
|
|
6
|
+
**Learning:** For forms that redirect to an external site (like Redsys), providing immediate feedback (disabling button, changing text to "Processing...") is essential to prevent multiple clicks and reassure the user that the redirection is in progress.
|
|
7
|
+
**Action:** Use `data-disable-with` or a simple `onsubmit` handler for all redirect-heavy forms.
|
data/.jules/sentinel.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
## 2025-05-24 - [Secure Configuration Handling]
|
|
2
|
+
**Vulnerability:** The configuration UI was unprotected, used plain text fields for secrets, and lacked input validation. It also used `YAML.load_file` which is potentially vulnerable to RCE.
|
|
3
|
+
**Learning:** For Rails Engines providing a configuration UI, it's crucial to mask secrets, validate inputs strictly, and ensure the UI is not accidentally exposed at the root route.
|
|
4
|
+
**Prevention:** Use `password_field` for secrets, `YAML.safe_load_file` for configuration loading, and move sensitive routes away from the engine root.
|
|
5
|
+
|
|
6
|
+
## 2025-05-25 - [Insecure Storage of Secrets in Configuration File]
|
|
7
|
+
**Vulnerability:** Secrets like `merchant_key` were being written to a plain YAML file (`config/redsys.yml`) in the application directory.
|
|
8
|
+
**Learning:** Secrets should never be persisted in plain text files within the application directory. They should be managed via environment variables or encrypted credentials.
|
|
9
|
+
**Prevention:** Exclude secrets from YAML serialization and prioritize loading from `ENV` or `Rails.application.credentials`.
|
|
10
|
+
|
|
11
|
+
## 2026-02-19 - [Unauthenticated Configuration Endpoint]
|
|
12
|
+
**Vulnerability:** The configuration UI in the Rails engine was accessible to unauthenticated users because it inherited directly from ActionController::Base and lacked any authentication filters.
|
|
13
|
+
**Learning:** Rails engines must provide a mechanism for host applications to secure engine-provided controllers, typically by making the parent controller configurable and providing authentication hooks.
|
|
14
|
+
**Prevention:** Implement a configurable 'parent_controller' and 'before_action' hooks in the engine's base controller to allow integration with the host application's authentication system.
|
|
15
|
+
|
|
16
|
+
## 2025-05-26 - [Insecure Random Order ID Generation]
|
|
17
|
+
**Vulnerability:** The gem used Ruby's insecure `rand` method with a small range (5 digits) to generate default order IDs, leading to high collision risk and lack of cryptographic security.
|
|
18
|
+
**Learning:** For identifiers in payment systems, cryptographically secure random number generators (like `SecureRandom`) should be used, and the range should be maximized to prevent collisions.
|
|
19
|
+
**Prevention:** Use `SecureRandom` instead of `rand` and utilize the full length allowed by the payment gateway (12 digits for Redsys) to ensure uniqueness and security.
|
|
20
|
+
|
|
21
|
+
## 2026-02-19 - [Sensitive Data Exposure in View]
|
|
22
|
+
**Vulnerability:** Rendering sensitive secrets in the 'value' attribute of a password input field allowed the cleartext secret to be retrieved from the page source.
|
|
23
|
+
**Learning:** Even when using `password_field`, Rails may render the `value` attribute if explicitly provided, exposing the secret in the HTML source. Sensitive fields should never have their values pre-filled in the HTML.
|
|
24
|
+
**Prevention:** Remove the `value` attribute from sensitive input fields to ensure they remain empty in the rendered HTML, relying on user input for updates.
|
data/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# RedsysRuby
|
|
2
|
+
|
|
3
|
+
A Ruby gem for making payments with Redsys using the HMAC-SHA256 signature algorithm.
|
|
4
|
+
|
|
5
|
+
**Note:** This documentation and some parts of the code use Spanish terms because Redsys is a payment provider that operates in Spain.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add this line to your application's Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem 'redsys-ruby'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
And then execute:
|
|
16
|
+
|
|
17
|
+
$ bundle install
|
|
18
|
+
|
|
19
|
+
Or install it yourself as:
|
|
20
|
+
|
|
21
|
+
$ gem install redsys-ruby
|
|
22
|
+
|
|
23
|
+
### Rails Integration
|
|
24
|
+
|
|
25
|
+
This gem includes a Rails Engine that provides a configuration interface, payment helpers, and premium success/failure pages.
|
|
26
|
+
|
|
27
|
+
#### 1. Mount the Engine
|
|
28
|
+
|
|
29
|
+
Add the following to your `config/routes.rb`:
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
mount RedsysRuby::Engine => "/redsys_ruby"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
#### 2. Configuration
|
|
36
|
+
|
|
37
|
+
For security, it is highly recommended to manage your secrets (like `merchant_key`) using **environment variables** or **Rails encrypted credentials**.
|
|
38
|
+
|
|
39
|
+
##### Using Environment Variables (Recommended)
|
|
40
|
+
|
|
41
|
+
Set the following environment variables in your system or using a gem like `dotenv`:
|
|
42
|
+
|
|
43
|
+
* `REDSYS_MERCHANT_KEY`: Your 256-bit merchant key (Base64 encoded).
|
|
44
|
+
* `REDSYS_MERCHANT_CODE`: Your 9-digit merchant code (FUC).
|
|
45
|
+
* `REDSYS_TERMINAL`: Your 3-digit terminal number.
|
|
46
|
+
* `REDSYS_ENVIRONMENT`: `test` or `production`.
|
|
47
|
+
|
|
48
|
+
##### Using Rails Credentials
|
|
49
|
+
|
|
50
|
+
You can also add these values to your encrypted credentials:
|
|
51
|
+
|
|
52
|
+
```yaml
|
|
53
|
+
# bin/rails credentials:edit
|
|
54
|
+
redsys:
|
|
55
|
+
merchant_key: "your_merchant_key_base64"
|
|
56
|
+
merchant_code: "999008881"
|
|
57
|
+
terminal: "001"
|
|
58
|
+
environment: "test"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
##### Using the Configuration UI
|
|
62
|
+
|
|
63
|
+
Alternatively, you can configure non-sensitive settings through the provided UI at `/redsys_ruby/configuration/edit`. Note that for security reasons, the `merchant_key` will **not** be saved to the `config/redsys.yml` file and should be provided via one of the methods above.
|
|
64
|
+
|
|
65
|
+
**Security Note:** By default, the configuration UI is unauthenticated. You should secure it by configuring an authentication method in an initializer:
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
# config/initializers/redsys.rb
|
|
69
|
+
RedsysRuby.configure do |config|
|
|
70
|
+
# Specify the parent controller for the engine (default: "ActionController::Base")
|
|
71
|
+
# Use your application's base controller to inherit its authentication and layout
|
|
72
|
+
config.parent_controller = "ApplicationController"
|
|
73
|
+
|
|
74
|
+
# Or provide a custom authentication block
|
|
75
|
+
config.before_configuration_action = -> {
|
|
76
|
+
# e.g., using Devise:
|
|
77
|
+
# authenticate_user!
|
|
78
|
+
# redirect_to root_path unless current_user.admin?
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
#### 3. Using the Payment Form Helper
|
|
84
|
+
|
|
85
|
+
In your views, you can use the `redsys_payment_form` helper to generate a payment form that redirects to Redsys:
|
|
86
|
+
|
|
87
|
+
```erb
|
|
88
|
+
<%= redsys_payment_form(amount: 10.50, order: "123456789012", description: "Product description") %>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The helper automatically includes:
|
|
92
|
+
- **Ds_SignatureVersion**
|
|
93
|
+
- **Ds_MerchantParameters**
|
|
94
|
+
- **Ds_Signature**
|
|
95
|
+
- **Ds_Merchant_UrlOK**: Points to the built-in premium success page.
|
|
96
|
+
- **Ds_Merchant_UrlKO**: Points to the built-in premium failure page.
|
|
97
|
+
|
|
98
|
+
### Premium Success & Failure Pages
|
|
99
|
+
|
|
100
|
+
The gem provides beautifully designed `Ok` and `KO` pages that match modern aesthetics, featuring:
|
|
101
|
+
- Inter typography.
|
|
102
|
+
- Smooth gradients and micro-animations.
|
|
103
|
+
- Responsive design.
|
|
104
|
+
- Backdrop blur effects.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
### Low-level Ruby Usage (Non-Rails)
|
|
109
|
+
|
|
110
|
+
## Development
|
|
111
|
+
|
|
112
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rspec` to run the tests.
|
|
113
|
+
|
|
114
|
+
## Contributing
|
|
115
|
+
|
|
116
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/smallpush/redsys-ruby.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RedsysRuby
|
|
4
|
+
class ConfigurationsController < ApplicationController
|
|
5
|
+
before_action :authenticate_configuration!
|
|
6
|
+
|
|
7
|
+
def edit
|
|
8
|
+
@configuration = Configuration.load
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def update
|
|
12
|
+
@configuration = Configuration.new(configuration_params)
|
|
13
|
+
if @configuration.save
|
|
14
|
+
redirect_to edit_configuration_path, notice: "Configuración actualizada correctamente."
|
|
15
|
+
else
|
|
16
|
+
render :edit, status: :unprocessable_entity
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def authenticate_configuration!
|
|
23
|
+
if RedsysRuby.before_configuration_action.is_a?(Proc)
|
|
24
|
+
instance_exec(&RedsysRuby.before_configuration_action)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def configuration_params
|
|
29
|
+
params.require(:configuration).permit(:merchant_key, :merchant_code, :terminal, :environment)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RedsysRuby
|
|
4
|
+
class PaymentsController < ApplicationController
|
|
5
|
+
def index
|
|
6
|
+
@order_id = Time.now.to_i.to_s[-12..-1] # Redsys order must be max 12 chars
|
|
7
|
+
@amount = 10.50
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def ok
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def ko
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RedsysRuby
|
|
4
|
+
module PaymentsHelper
|
|
5
|
+
def redsys_payment_form(amount:, order: nil, description: nil, button_text: "Pagar con Redsys", button_class: "redsys-submit")
|
|
6
|
+
config = Configuration.load
|
|
7
|
+
tpv = TPV.new(merchant_key: config.merchant_key)
|
|
8
|
+
|
|
9
|
+
order ||= SecureRandom.random_number(10**12).to_s.rjust(12, "0")
|
|
10
|
+
|
|
11
|
+
params = {
|
|
12
|
+
Ds_Merchant_Amount: (amount.to_f * 100).to_i.to_s,
|
|
13
|
+
Ds_Merchant_Order: order.to_s,
|
|
14
|
+
Ds_Merchant_MerchantCode: config.merchant_code,
|
|
15
|
+
Ds_Merchant_Currency: "978", # EUR
|
|
16
|
+
Ds_Merchant_TransactionType: "0", # Autorización
|
|
17
|
+
Ds_Merchant_Terminal: config.terminal,
|
|
18
|
+
Ds_Merchant_MerchantURL: "", # Should be configured or passed
|
|
19
|
+
Ds_Merchant_UrlOK: RedsysRuby::Engine.routes.url_helpers.ok_payments_url(host: request.base_url),
|
|
20
|
+
Ds_Merchant_UrlKO: RedsysRuby::Engine.routes.url_helpers.ko_payments_url(host: request.base_url)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
params[:Ds_Merchant_ProductDescription] = description if description.present?
|
|
24
|
+
|
|
25
|
+
payment_data = tpv.payment_data(params)
|
|
26
|
+
url = config.environment == "production" ? TPV::PRODUCTION_URL : TPV::TEST_URL
|
|
27
|
+
|
|
28
|
+
form_with url: url, method: :post, local: true, html: { id: "redsys_payment_form" } do |f|
|
|
29
|
+
concat f.hidden_field :Ds_SignatureVersion, value: payment_data[:Ds_SignatureVersion]
|
|
30
|
+
concat f.hidden_field :Ds_MerchantParameters, value: payment_data[:Ds_MerchantParameters]
|
|
31
|
+
concat f.hidden_field :Ds_Signature, value: payment_data[:Ds_Signature]
|
|
32
|
+
concat f.submit button_text,
|
|
33
|
+
class: button_class,
|
|
34
|
+
aria: { label: "#{button_text} - Redirigir a pasarela de pago segura" },
|
|
35
|
+
data: { disable_with: "Procesando..." }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RedsysRuby
|
|
4
|
+
class Configuration
|
|
5
|
+
include ActiveModel::Model
|
|
6
|
+
include ActiveModel::Validations
|
|
7
|
+
|
|
8
|
+
attr_accessor :merchant_key, :merchant_code, :terminal, :environment
|
|
9
|
+
|
|
10
|
+
validates :merchant_key, :merchant_code, :terminal, :environment, presence: true
|
|
11
|
+
validates :merchant_code, format: { with: /\A\d{9}\z/, message: "debe tener exactamente 9 dígitos" }
|
|
12
|
+
validates :terminal, format: { with: /\A\d{3}\z/, message: "debe tener exactamente 3 dígitos" }
|
|
13
|
+
validates :environment, inclusion: { in: %w[test production], message: "debe ser 'test' o 'production'" }
|
|
14
|
+
|
|
15
|
+
CONFIG_PATH = Rails.root.join("config", "redsys.yml")
|
|
16
|
+
|
|
17
|
+
def self.load
|
|
18
|
+
config = {}
|
|
19
|
+
if File.exist?(CONFIG_PATH)
|
|
20
|
+
config = (YAML.safe_load_file(CONFIG_PATH, aliases: true) || {})[Rails.env] || {}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
creds = Rails.application.credentials.redsys rescue {}
|
|
24
|
+
|
|
25
|
+
new(
|
|
26
|
+
merchant_key: ENV["REDSYS_MERCHANT_KEY"] || creds[:merchant_key] || config["merchant_key"],
|
|
27
|
+
merchant_code: ENV["REDSYS_MERCHANT_CODE"] || creds[:merchant_code] || config["merchant_code"],
|
|
28
|
+
terminal: ENV["REDSYS_TERMINAL"] || creds[:terminal] || config["terminal"],
|
|
29
|
+
environment: ENV["REDSYS_ENVIRONMENT"] || creds[:environment] || config["environment"] || "test"
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def merchant_key_from_secure_source?
|
|
34
|
+
ENV["REDSYS_MERCHANT_KEY"].present? || (Rails.application.credentials.redsys&.dig(:merchant_key).present? rescue false)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def save
|
|
38
|
+
return false unless valid?
|
|
39
|
+
|
|
40
|
+
config_data = {}
|
|
41
|
+
if File.exist?(CONFIG_PATH)
|
|
42
|
+
config_data = YAML.safe_load_file(CONFIG_PATH, aliases: true) || {}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# We exclude merchant_key from the YAML file for security reasons.
|
|
46
|
+
# Secrets should be managed via environment variables or encrypted credentials.
|
|
47
|
+
data_to_save = attributes.except("merchant_key")
|
|
48
|
+
|
|
49
|
+
config_data[Rails.env] = data_to_save
|
|
50
|
+
File.write(CONFIG_PATH, config_data.to_yaml)
|
|
51
|
+
true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def attributes
|
|
55
|
+
{
|
|
56
|
+
"merchant_key" => merchant_key,
|
|
57
|
+
"merchant_code" => merchant_code,
|
|
58
|
+
"terminal" => terminal,
|
|
59
|
+
"environment" => environment
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
.redsys-config-container {
|
|
3
|
+
max-width: 600px;
|
|
4
|
+
margin: 40px auto;
|
|
5
|
+
padding: 30px;
|
|
6
|
+
background: rgba(255, 255, 255, 0.95);
|
|
7
|
+
border-radius: 16px;
|
|
8
|
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
|
9
|
+
backdrop-filter: blur(10px);
|
|
10
|
+
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.redsys-header {
|
|
14
|
+
text-align: center;
|
|
15
|
+
margin-bottom: 30px;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.redsys-header h1 {
|
|
19
|
+
color: #1a1a1a;
|
|
20
|
+
font-size: 24px;
|
|
21
|
+
font-weight: 700;
|
|
22
|
+
margin-bottom: 8px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.redsys-header p {
|
|
26
|
+
color: #666;
|
|
27
|
+
font-size: 14px;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.redsys-form-group {
|
|
31
|
+
margin-bottom: 20px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.redsys-label {
|
|
35
|
+
display: block;
|
|
36
|
+
font-size: 14px;
|
|
37
|
+
font-weight: 600;
|
|
38
|
+
color: #444;
|
|
39
|
+
margin-bottom: 8px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.redsys-input, .redsys-select {
|
|
43
|
+
width: 100%;
|
|
44
|
+
padding: 12px 16px;
|
|
45
|
+
border: 1px solid #ddd;
|
|
46
|
+
border-radius: 8px;
|
|
47
|
+
font-size: 15px;
|
|
48
|
+
transition: all 0.3s ease;
|
|
49
|
+
box-sizing: border-box;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.redsys-input:focus, .redsys-select:focus {
|
|
53
|
+
outline: none;
|
|
54
|
+
border-color: #3b82f6;
|
|
55
|
+
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.redsys-submit {
|
|
59
|
+
width: 100%;
|
|
60
|
+
padding: 14px;
|
|
61
|
+
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
|
62
|
+
color: white;
|
|
63
|
+
border: none;
|
|
64
|
+
border-radius: 8px;
|
|
65
|
+
font-size: 16px;
|
|
66
|
+
font-weight: 600;
|
|
67
|
+
cursor: pointer;
|
|
68
|
+
transition: transform 0.2s, box-shadow 0.2s;
|
|
69
|
+
margin-top: 10px;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.redsys-submit:hover {
|
|
73
|
+
transform: translateY(-1px);
|
|
74
|
+
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.redsys-notice {
|
|
78
|
+
padding: 12px 16px;
|
|
79
|
+
background: #ecfdf5;
|
|
80
|
+
color: #065f46;
|
|
81
|
+
border-radius: 8px;
|
|
82
|
+
margin-bottom: 20px;
|
|
83
|
+
font-size: 14px;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.redsys-errors {
|
|
87
|
+
padding: 12px 16px;
|
|
88
|
+
background: #fef2f2;
|
|
89
|
+
color: #991b1b;
|
|
90
|
+
border-radius: 8px;
|
|
91
|
+
margin-bottom: 20px;
|
|
92
|
+
font-size: 14px;
|
|
93
|
+
}
|
|
94
|
+
</style>
|
|
95
|
+
|
|
96
|
+
<div class="redsys-config-container">
|
|
97
|
+
<div class="redsys-header">
|
|
98
|
+
<h1>Configurar Redsys</h1>
|
|
99
|
+
<p>Introduce tus credenciales del TPVVirtual</p>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<% if flash[:notice] %>
|
|
103
|
+
<div class="redsys-notice">
|
|
104
|
+
<%= flash[:notice] %>
|
|
105
|
+
</div>
|
|
106
|
+
<% end %>
|
|
107
|
+
|
|
108
|
+
<% if @configuration.errors.any? %>
|
|
109
|
+
<div class="redsys-errors">
|
|
110
|
+
<ul>
|
|
111
|
+
<% @configuration.errors.full_messages.each do |msg| %>
|
|
112
|
+
<li><%= msg %></li>
|
|
113
|
+
<% end %>
|
|
114
|
+
</ul>
|
|
115
|
+
</div>
|
|
116
|
+
<% end %>
|
|
117
|
+
|
|
118
|
+
<%= form_with model: @configuration, url: configuration_path, method: :patch, local: true do |f| %>
|
|
119
|
+
<div class="redsys-form-group">
|
|
120
|
+
<%= f.label :merchant_key, "Merchant Key (FUC Key)", class: "redsys-label" %>
|
|
121
|
+
<% if @configuration.merchant_key_from_secure_source? %>
|
|
122
|
+
<div class="redsys-input" style="background: #ecfdf5; color: #065f46; border-color: #10b981; display: flex; align-items: center; gap: 8px;">
|
|
123
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>
|
|
124
|
+
Configurada mediante variable de entorno o credenciales
|
|
125
|
+
</div>
|
|
126
|
+
<% else %>
|
|
127
|
+
<%= f.password_field :merchant_key, class: "redsys-input", placeholder: "Ej: sq7H5YzEBhyUTLe6BArsth4i..." %>
|
|
128
|
+
<p style="font-size: 12px; color: #b91c1c; margin-top: 8px; display: flex; align-items: flex-start; gap: 6px;">
|
|
129
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink: 0; margin-top: 2px;"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>
|
|
130
|
+
<span>Por seguridad, se recomienda configurar esta clave mediante la variable de entorno <code>REDSYS_MERCHANT_KEY</code> o en las credenciales encriptadas de Rails.</span>
|
|
131
|
+
</p>
|
|
132
|
+
<% end %>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<div class="redsys-form-group">
|
|
136
|
+
<%= f.label :merchant_code, "Código de Comercio (FUC)", class: "redsys-label" %>
|
|
137
|
+
<%= f.text_field :merchant_code, class: "redsys-input", placeholder: "Ej: 999008881" %>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<div class="redsys-form-group">
|
|
141
|
+
<%= f.label :terminal, "Terminal", class: "redsys-label" %>
|
|
142
|
+
<%= f.text_field :terminal, class: "redsys-input", placeholder: "Ej: 001" %>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<div class="redsys-form-group">
|
|
146
|
+
<%= f.label :environment, "Entorno", class: "redsys-label" %>
|
|
147
|
+
<%= f.select :environment, [["Pruebas", "test"], ["Producción", "production"]], {}, class: "redsys-select" %>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<%= f.submit "Guardar Configuración", class: "redsys-submit" %>
|
|
151
|
+
<% end %>
|
|
152
|
+
</div>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
.redsys-demo-container {
|
|
3
|
+
max-width: 600px;
|
|
4
|
+
margin: 40px auto;
|
|
5
|
+
padding: 30px;
|
|
6
|
+
background: white;
|
|
7
|
+
border-radius: 16px;
|
|
8
|
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
|
9
|
+
text-align: center;
|
|
10
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
11
|
+
}
|
|
12
|
+
.redsys-amount {
|
|
13
|
+
font-size: 48px;
|
|
14
|
+
font-weight: 800;
|
|
15
|
+
color: #1a1a1a;
|
|
16
|
+
margin: 20px 0;
|
|
17
|
+
}
|
|
18
|
+
.redsys-submit {
|
|
19
|
+
display: inline-block;
|
|
20
|
+
padding: 14px 28px;
|
|
21
|
+
background: #2563eb;
|
|
22
|
+
color: white;
|
|
23
|
+
text-decoration: none;
|
|
24
|
+
border-radius: 8px;
|
|
25
|
+
font-weight: 600;
|
|
26
|
+
border: none;
|
|
27
|
+
cursor: pointer;
|
|
28
|
+
transition: all 0.2s;
|
|
29
|
+
}
|
|
30
|
+
.redsys-submit:hover {
|
|
31
|
+
background: #1d4ed8;
|
|
32
|
+
transform: translateY(-1px);
|
|
33
|
+
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.2);
|
|
34
|
+
}
|
|
35
|
+
.redsys-submit:active {
|
|
36
|
+
transform: translateY(0) scale(0.98);
|
|
37
|
+
}
|
|
38
|
+
.redsys-submit:focus-visible {
|
|
39
|
+
outline: 2px solid #2563eb;
|
|
40
|
+
outline-offset: 2px;
|
|
41
|
+
}
|
|
42
|
+
.redsys-secure-notice {
|
|
43
|
+
display: flex;
|
|
44
|
+
align-items: center;
|
|
45
|
+
justify-content: center;
|
|
46
|
+
gap: 8px;
|
|
47
|
+
margin-top: 16px;
|
|
48
|
+
color: #059669;
|
|
49
|
+
font-size: 14px;
|
|
50
|
+
font-weight: 500;
|
|
51
|
+
}
|
|
52
|
+
</style>
|
|
53
|
+
|
|
54
|
+
<div class="redsys-demo-container">
|
|
55
|
+
<h1>Prueba de Pago</h1>
|
|
56
|
+
<p>Estás a punto de realizar un pago de prueba con Redsys.</p>
|
|
57
|
+
|
|
58
|
+
<div class="redsys-amount" aria-label="Importe a pagar: <%= number_to_currency(@amount, unit: '€') %>">
|
|
59
|
+
<%= number_to_currency(@amount, unit: "€") %>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<p>ID de Pedido: <strong><%= @order_id %></strong></p>
|
|
63
|
+
|
|
64
|
+
<%= redsys_payment_form(amount: @amount, order: @order_id, description: "Compra de prueba") %>
|
|
65
|
+
|
|
66
|
+
<div class="redsys-secure-notice">
|
|
67
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
68
|
+
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
|
69
|
+
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
|
70
|
+
</svg>
|
|
71
|
+
<span>Pago 100% Seguro</span>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<p style="margin-top: 24px; font-size: 13px; color: #64748b; line-height: 1.5;">
|
|
75
|
+
Al hacer clic, serás redirigido de forma segura al entorno de <strong><%= RedsysRuby::Configuration.load.environment %></strong> de Redsys para completar tu transacción.
|
|
76
|
+
</p>
|
|
77
|
+
</div>
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');
|
|
3
|
+
|
|
4
|
+
.redsys-payment-container {
|
|
5
|
+
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
|
6
|
+
min-height: 80vh;
|
|
7
|
+
display: flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
justify-content: center;
|
|
10
|
+
background: radial-gradient(circle at top right, #fff1f2, #f1f5f9);
|
|
11
|
+
padding: 2rem;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.redsys-card {
|
|
15
|
+
background: rgba(255, 255, 255, 0.8);
|
|
16
|
+
backdrop-filter: blur(12px);
|
|
17
|
+
-webkit-backdrop-filter: blur(12px);
|
|
18
|
+
border-radius: 24px;
|
|
19
|
+
padding: 3rem;
|
|
20
|
+
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.05);
|
|
21
|
+
max-width: 480px;
|
|
22
|
+
width: 100%;
|
|
23
|
+
text-align: center;
|
|
24
|
+
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
25
|
+
animation: slideUp 0.6s cubic-bezier(0.16, 1, 0.3, 1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@keyframes slideUp {
|
|
29
|
+
from { opacity: 0; transform: translateY(20px); }
|
|
30
|
+
to { opacity: 1; transform: translateY(0); }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.error-icon-wrapper {
|
|
34
|
+
width: 80px;
|
|
35
|
+
height: 80px;
|
|
36
|
+
background: #fef2f2;
|
|
37
|
+
border-radius: 50%;
|
|
38
|
+
display: flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
justify-content: center;
|
|
41
|
+
margin: 0 auto 1.5rem;
|
|
42
|
+
animation: scaleIn 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) 0.2s both;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@keyframes scaleIn {
|
|
46
|
+
from { transform: scale(0); }
|
|
47
|
+
to { transform: scale(1); }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.error-icon {
|
|
51
|
+
color: #ef4444;
|
|
52
|
+
width: 40px;
|
|
53
|
+
height: 40px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.redsys-title {
|
|
57
|
+
color: #1e293b;
|
|
58
|
+
font-size: 1.875rem;
|
|
59
|
+
font-weight: 700;
|
|
60
|
+
margin-bottom: 1rem;
|
|
61
|
+
letter-spacing: -0.025em;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.redsys-description {
|
|
65
|
+
color: #64748b;
|
|
66
|
+
font-size: 1.125rem;
|
|
67
|
+
line-height: 1.6;
|
|
68
|
+
margin-bottom: 2rem;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.redsys-actions {
|
|
72
|
+
display: flex;
|
|
73
|
+
flex-direction: column;
|
|
74
|
+
gap: 1rem;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.redsys-button {
|
|
78
|
+
display: inline-block;
|
|
79
|
+
background: #0f172a;
|
|
80
|
+
color: white;
|
|
81
|
+
padding: 0.875rem 2rem;
|
|
82
|
+
border-radius: 12px;
|
|
83
|
+
font-weight: 600;
|
|
84
|
+
text-decoration: none;
|
|
85
|
+
transition: all 0.2s ease;
|
|
86
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.redsys-button:hover {
|
|
90
|
+
transform: translateY(-2px);
|
|
91
|
+
background: #1e293b;
|
|
92
|
+
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.redsys-button.secondary {
|
|
96
|
+
background: transparent;
|
|
97
|
+
color: #475569;
|
|
98
|
+
border: 1px solid #e2e8f0;
|
|
99
|
+
box-shadow: none;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.redsys-button.secondary:hover {
|
|
103
|
+
background: #f8fafc;
|
|
104
|
+
border-color: #cbd5e1;
|
|
105
|
+
}
|
|
106
|
+
</style>
|
|
107
|
+
|
|
108
|
+
<div class="redsys-payment-container">
|
|
109
|
+
<div class="redsys-card">
|
|
110
|
+
<div class="error-icon-wrapper">
|
|
111
|
+
<svg class="error-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
112
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12"></path>
|
|
113
|
+
</svg>
|
|
114
|
+
</div>
|
|
115
|
+
<h1 class="redsys-title">Pago Cancelado</h1>
|
|
116
|
+
<p class="redsys-description">
|
|
117
|
+
No se ha podido completar la transacción. Por favor, revisa los datos de tu tarjeta o inténtalo de nuevo más tarde.
|
|
118
|
+
</p>
|
|
119
|
+
<div class="redsys-actions">
|
|
120
|
+
<a href="/redsys_ruby/payments" class="redsys-button">Intentar de nuevo</a>
|
|
121
|
+
<a href="/" class="redsys-button secondary">Volver al inicio</a>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');
|
|
3
|
+
|
|
4
|
+
.redsys-payment-container {
|
|
5
|
+
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
|
6
|
+
min-height: 80vh;
|
|
7
|
+
display: flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
justify-content: center;
|
|
10
|
+
background: radial-gradient(circle at top right, #f8fafc, #f1f5f9);
|
|
11
|
+
padding: 2rem;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.redsys-card {
|
|
15
|
+
background: rgba(255, 255, 255, 0.8);
|
|
16
|
+
backdrop-filter: blur(12px);
|
|
17
|
+
-webkit-backdrop-filter: blur(12px);
|
|
18
|
+
border-radius: 24px;
|
|
19
|
+
padding: 3rem;
|
|
20
|
+
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.05);
|
|
21
|
+
max-width: 480px;
|
|
22
|
+
width: 100%;
|
|
23
|
+
text-align: center;
|
|
24
|
+
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
25
|
+
animation: slideUp 0.6s cubic-bezier(0.16, 1, 0.3, 1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@keyframes slideUp {
|
|
29
|
+
from { opacity: 0; transform: translateY(20px); }
|
|
30
|
+
to { opacity: 1; transform: translateY(0); }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.success-icon-wrapper {
|
|
34
|
+
width: 80px;
|
|
35
|
+
height: 80px;
|
|
36
|
+
background: #ecfdf5;
|
|
37
|
+
border-radius: 50%;
|
|
38
|
+
display: flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
justify-content: center;
|
|
41
|
+
margin: 0 auto 1.5rem;
|
|
42
|
+
animation: scaleIn 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) 0.2s both;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@keyframes scaleIn {
|
|
46
|
+
from { transform: scale(0); }
|
|
47
|
+
to { transform: scale(1); }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.success-icon {
|
|
51
|
+
color: #10b981;
|
|
52
|
+
width: 40px;
|
|
53
|
+
height: 40px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.redsys-title {
|
|
57
|
+
color: #1e293b;
|
|
58
|
+
font-size: 1.875rem;
|
|
59
|
+
font-weight: 700;
|
|
60
|
+
margin-bottom: 1rem;
|
|
61
|
+
letter-spacing: -0.025em;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.redsys-description {
|
|
65
|
+
color: #64748b;
|
|
66
|
+
font-size: 1.125rem;
|
|
67
|
+
line-height: 1.6;
|
|
68
|
+
margin-bottom: 2rem;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.redsys-button {
|
|
72
|
+
display: inline-block;
|
|
73
|
+
background: #0f172a;
|
|
74
|
+
color: white;
|
|
75
|
+
padding: 0.875rem 2rem;
|
|
76
|
+
border-radius: 12px;
|
|
77
|
+
font-weight: 600;
|
|
78
|
+
text-decoration: none;
|
|
79
|
+
transition: all 0.2s ease;
|
|
80
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.redsys-button:hover {
|
|
84
|
+
transform: translateY(-2px);
|
|
85
|
+
background: #1e293b;
|
|
86
|
+
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.redsys-button:active {
|
|
90
|
+
transform: translateY(0);
|
|
91
|
+
}
|
|
92
|
+
</style>
|
|
93
|
+
|
|
94
|
+
<div class="redsys-payment-container">
|
|
95
|
+
<div class="redsys-card">
|
|
96
|
+
<div class="success-icon-wrapper">
|
|
97
|
+
<svg class="success-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
98
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7"></path>
|
|
99
|
+
</svg>
|
|
100
|
+
</div>
|
|
101
|
+
<h1 class="redsys-title">¡Pago Completado!</h1>
|
|
102
|
+
<p class="redsys-description">
|
|
103
|
+
Tu transacción se ha procesado correctamente. En breve recibirás un correo electrónico con los detalles de tu pedido.
|
|
104
|
+
</p>
|
|
105
|
+
<a href="/" class="redsys-button">Volver al inicio</a>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
require "base64"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module RedsysRuby
|
|
8
|
+
class TPV
|
|
9
|
+
PRODUCTION_URL = "https://sis.redsys.es/sis/realizarPago"
|
|
10
|
+
TEST_URL = "https://sis-t.redsys.es:25443/sis/realizarPago"
|
|
11
|
+
|
|
12
|
+
attr_reader :merchant_key
|
|
13
|
+
|
|
14
|
+
def initialize(merchant_key:)
|
|
15
|
+
@merchant_key = merchant_key
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def encrypt_3des(order, key)
|
|
19
|
+
cipher = OpenSSL::Cipher.new("DES-EDE3-CBC")
|
|
20
|
+
cipher.encrypt
|
|
21
|
+
cipher.key = key[0..23]
|
|
22
|
+
cipher.iv = "\0" * 8
|
|
23
|
+
cipher.padding = 0
|
|
24
|
+
|
|
25
|
+
# Redsys uses 8-byte blocks. The order must be padded with null bytes to a multiple of 8.
|
|
26
|
+
padded_order = order.ljust((order.length + 7) / 8 * 8, "\0")
|
|
27
|
+
cipher.update(padded_order) + cipher.final
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def generate_merchant_parameters(params)
|
|
31
|
+
json_params = params.to_json
|
|
32
|
+
Base64.strict_encode64(json_params)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def generate_merchant_signature(order, merchant_parameters_64)
|
|
36
|
+
digest = calculate_digest(order, merchant_parameters_64)
|
|
37
|
+
Base64.strict_encode64(digest)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def payment_data(params)
|
|
41
|
+
params = params.transform_keys(&:to_s)
|
|
42
|
+
merchant_parameters_64 = generate_merchant_parameters(params)
|
|
43
|
+
order = params["Ds_Merchant_Order"]
|
|
44
|
+
|
|
45
|
+
{
|
|
46
|
+
Ds_SignatureVersion: "HMAC_SHA256_V1",
|
|
47
|
+
Ds_MerchantParameters: merchant_parameters_64,
|
|
48
|
+
Ds_Signature: generate_merchant_signature(order.to_s, merchant_parameters_64)
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def generate_merchant_signature_notif(merchant_parameters_64)
|
|
53
|
+
# For notifications, we need to extract the order from the decoded parameters.
|
|
54
|
+
decoded_params = decode_parameters(merchant_parameters_64)
|
|
55
|
+
order = decoded_params["Ds_Order"] || decoded_params["Ds_Merchant_Order"]
|
|
56
|
+
|
|
57
|
+
raise ArgumentError, "Order is missing in merchant parameters" if order.nil?
|
|
58
|
+
|
|
59
|
+
digest = calculate_digest(order, merchant_parameters_64)
|
|
60
|
+
Base64.urlsafe_encode64(digest)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def valid_signature?(merchant_parameters_64, signature)
|
|
64
|
+
expected_signature = generate_merchant_signature_notif(merchant_parameters_64)
|
|
65
|
+
# We should use a constant-time comparison here for security
|
|
66
|
+
secure_compare(expected_signature, signature)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def decode_parameters(merchant_parameters_64)
|
|
70
|
+
JSON.parse(Base64.decode64(merchant_parameters_64))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
# Encrypts the order number with the merchant key using 3DES
|
|
76
|
+
def encrypt_3des(order, key)
|
|
77
|
+
cipher = OpenSSL::Cipher.new("DES-EDE3-CBC")
|
|
78
|
+
cipher.encrypt
|
|
79
|
+
cipher.key = key[0..23]
|
|
80
|
+
cipher.iv = "\0" * 8
|
|
81
|
+
cipher.padding = 0
|
|
82
|
+
|
|
83
|
+
padded_order = order.ljust((order.length + 7) / 8 * 8, "\0")
|
|
84
|
+
cipher.update(padded_order) + cipher.final
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def calculate_digest(order, merchant_parameters_64)
|
|
88
|
+
# 1. Decode the merchant key
|
|
89
|
+
decoded_key = Base64.decode64(@merchant_key)
|
|
90
|
+
|
|
91
|
+
# 2. Derive the key for this order
|
|
92
|
+
derived_key = encrypt_3des(order, decoded_key)
|
|
93
|
+
|
|
94
|
+
# 3. Calculate HMAC-SHA256
|
|
95
|
+
OpenSSL::HMAC.digest(OpenSSL::Digest.new("sha256"), derived_key, merchant_parameters_64)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def secure_compare(a, b)
|
|
99
|
+
return false if a.empty? || b.empty? || a.bytesize != b.bytesize
|
|
100
|
+
|
|
101
|
+
OpenSSL.fixed_length_secure_compare(a, b)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
data/lib/redsys-ruby.rb
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require_relative "redsys-ruby/version"
|
|
5
|
+
require_relative "redsys-ruby/tpv"
|
|
6
|
+
require_relative "redsys-ruby/engine" if defined?(Rails)
|
|
7
|
+
|
|
8
|
+
module RedsysRuby
|
|
9
|
+
class Error < StandardError; end
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def configure
|
|
13
|
+
yield self
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
attr_accessor :parent_controller
|
|
17
|
+
attr_accessor :before_configuration_action
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Set defaults
|
|
21
|
+
self.parent_controller = "ActionController::Base"
|
|
22
|
+
self.before_configuration_action = nil
|
|
23
|
+
end
|
data/sig/redsys_ruby.rbs
ADDED
metadata
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: redsys-ruby
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- google-labs-jules[bot]
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-02-19 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: railties
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '7.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '7.0'
|
|
27
|
+
description: Implement Redsys HMAC SHA256 signature and payment parameters handling.
|
|
28
|
+
email:
|
|
29
|
+
- 161369871+google-labs-jules[bot]@users.noreply.github.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- ".jules/palette.md"
|
|
35
|
+
- ".jules/sentinel.md"
|
|
36
|
+
- README.md
|
|
37
|
+
- Rakefile
|
|
38
|
+
- app/controllers/redsys_ruby/application_controller.rb
|
|
39
|
+
- app/controllers/redsys_ruby/configurations_controller.rb
|
|
40
|
+
- app/controllers/redsys_ruby/payments_controller.rb
|
|
41
|
+
- app/helpers/redsys_ruby/payments_helper.rb
|
|
42
|
+
- app/models/redsys_ruby/configuration.rb
|
|
43
|
+
- app/views/redsys_ruby/configurations/edit.html.erb
|
|
44
|
+
- app/views/redsys_ruby/payments/index.html.erb
|
|
45
|
+
- app/views/redsys_ruby/payments/ko.html.erb
|
|
46
|
+
- app/views/redsys_ruby/payments/ok.html.erb
|
|
47
|
+
- config/routes.rb
|
|
48
|
+
- lib/redsys-ruby.rb
|
|
49
|
+
- lib/redsys-ruby/engine.rb
|
|
50
|
+
- lib/redsys-ruby/tpv.rb
|
|
51
|
+
- lib/redsys-ruby/version.rb
|
|
52
|
+
- sig/redsys_ruby.rbs
|
|
53
|
+
homepage: https://github.com/google-labs/redsys-ruby
|
|
54
|
+
licenses: []
|
|
55
|
+
metadata:
|
|
56
|
+
homepage_uri: https://github.com/google-labs/redsys-ruby
|
|
57
|
+
source_code_uri: https://github.com/google-labs/redsys-ruby
|
|
58
|
+
post_install_message:
|
|
59
|
+
rdoc_options: []
|
|
60
|
+
require_paths:
|
|
61
|
+
- lib
|
|
62
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - ">="
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: 3.2.0
|
|
67
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
68
|
+
requirements:
|
|
69
|
+
- - ">="
|
|
70
|
+
- !ruby/object:Gem::Version
|
|
71
|
+
version: '0'
|
|
72
|
+
requirements: []
|
|
73
|
+
rubygems_version: 3.4.20
|
|
74
|
+
signing_key:
|
|
75
|
+
specification_version: 4
|
|
76
|
+
summary: A Ruby gem for making payments with Redsys.
|
|
77
|
+
test_files: []
|