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 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.
@@ -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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RedsysRuby
4
+ class ApplicationController < RedsysRuby.parent_controller.constantize
5
+ layout "application"
6
+ end
7
+ end
@@ -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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ RedsysRuby::Engine.routes.draw do
4
+ resource :configuration, only: [:edit, :update]
5
+ resources :payments, only: [:index] do
6
+ collection do
7
+ get :ok
8
+ get :ko
9
+ end
10
+ end
11
+ root to: "payments#index"
12
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RedsysRuby
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace RedsysRuby
6
+ end
7
+ end
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RedsysRuby
4
+ VERSION = "0.1.0"
5
+ end
@@ -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
@@ -0,0 +1,4 @@
1
+ module RedsysRuby
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
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: []