google_pay_ruby 1.0.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: 11c693c4ee510867c2a52137125cbb477c076e966ebb8341643ade2138a85418
4
+ data.tar.gz: aae173c01a2bb5f41255e6c372e947dbfcb4e0461a13234aefe2ab2ca77accba
5
+ SHA512:
6
+ metadata.gz: a15ac114c7aece47d4e96e89dbe59ed0f52913fac131d8bc2854ac73c992c2323c8ba4c77440b48fb69124f5e4b069cf7b2a17a5c43d7e4fc9462fa8c86bcab7
7
+ data.tar.gz: 9ecb09181a31652d1bb2fada7f3251bf7d247cb5a09fe32d7fabcebe0f6ef54d8a29931aae810338765f6a6a78c432742eff1d596ee35f04d72c4b4b03e88875
data/CHANGELOG.md ADDED
@@ -0,0 +1,25 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.0.0] - 2026-02-05
9
+
10
+ ### Added
11
+ - Initial release
12
+ - ECv2 protocol support for Google Pay token decryption
13
+ - Multiple merchant key support for key rotation
14
+ - Pure Ruby implementation using OpenSSL
15
+ - Comprehensive error handling with detailed error messages
16
+ - Support for both string and symbol keys in token hashes
17
+
18
+ ### Features
19
+ - `GooglePaymentMethodTokenContext` class for managing decryption context
20
+ - `EcV2DecryptionStrategy` class implementing ECv2 decryption algorithm
21
+ - `GooglePaymentDecryptionError` exception class with detailed error tracking
22
+ - ECDH shared secret computation
23
+ - HKDF key derivation
24
+ - HMAC verification
25
+ - AES-256-CTR decryption
data/LICENSE.txt ADDED
@@ -0,0 +1,9 @@
1
+
2
+
3
+ Copyright <2026> <Sahil Gadimbayli>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,158 @@
1
+ # GooglePayRuby
2
+
3
+ A Ruby utility for securely decrypting Google Pay PaymentMethodTokens using the ECv2 protocol. This gem is inspired from [Basis Theory google-pay-js](https://github.com/Basis-Theory/google-pay-js) library.
4
+
5
+ ## Features
6
+
7
+ - **Google Pay PaymentMethodToken Decryption**: Securely decrypt user-authorized Google Pay transaction tokens using easy-to-use interfaces
8
+ - **Key Rotation Support**: Handle multiple private keys simultaneously to support seamless key rotation without missing payments
9
+ - **Pure Ruby Implementation**: No external dependencies beyond OpenSSL (included in Ruby standard library)
10
+ - **ECv2 Protocol Support**: Implements Google Pay's ECv2 encryption protocol
11
+
12
+ ## Installation
13
+
14
+ Add this line to your application's Gemfile:
15
+
16
+ ```ruby
17
+ gem 'google_pay_ruby'
18
+ ```
19
+
20
+ And then execute:
21
+
22
+ ```bash
23
+ bundle install
24
+ ```
25
+
26
+ Or install it yourself as:
27
+
28
+ ```bash
29
+ gem install google_pay_ruby
30
+ ```
31
+
32
+ ## Google Pay Setup
33
+
34
+ Follow [Google Pay API guides](https://developers.google.com/pay/api) for your platform. To use this library, you need to:
35
+
36
+ 1. Be PCI Level 1 certified
37
+ 2. Choose tokenization type `DIRECT`
38
+ 3. Generate and register your encryption keys with Google
39
+
40
+ ⚠️ **Important**: If you are not PCI Level 1 certified, consider using `PAYMENT_GATEWAY` tokenization type or contact a payment gateway provider.
41
+
42
+ ## Usage
43
+
44
+ ### Basic Example
45
+
46
+ ```ruby
47
+ require 'google_pay_ruby'
48
+
49
+ # Load your private key (from file, KMS, secrets manager, etc.)
50
+ private_key_pem = File.read('path/to/your/private_key.pem')
51
+
52
+ # Create the decryption context
53
+ context = GooglePayRuby::GooglePaymentMethodTokenContext.new(
54
+ merchants: [
55
+ {
56
+ identifier: 'my-merchant-identifier', # Optional, for debugging
57
+ private_key_pem: private_key_pem
58
+ }
59
+ ]
60
+ )
61
+
62
+ # Get the token from Google Pay API response
63
+ token = {
64
+ 'protocolVersion' => 'ECv2',
65
+ 'signature' => '...',
66
+ 'signedMessage' => '{"encryptedMessage":"...","ephemeralPublicKey":"...","tag":"..."}'
67
+ }
68
+
69
+ # Decrypt the token
70
+ begin
71
+ decrypted_data = context.decrypt(token)
72
+
73
+ puts "PAN: #{decrypted_data['paymentMethodDetails']['pan']}"
74
+ puts "Expiration: #{decrypted_data['paymentMethodDetails']['expirationMonth']}/#{decrypted_data['paymentMethodDetails']['expirationYear']}"
75
+ puts "Cryptogram: #{decrypted_data['paymentMethodDetails']['cryptogram']}"
76
+ rescue GooglePayRuby::GooglePaymentDecryptionError => e
77
+ puts "Decryption failed: #{e.message}"
78
+ end
79
+ ```
80
+
81
+ ### Key Rotation Support
82
+
83
+ Handle key rotation gracefully by providing multiple private keys:
84
+
85
+ ```ruby
86
+ context = GooglePayRuby::GooglePaymentMethodTokenContext.new(
87
+ merchants: [
88
+ {
89
+ identifier: 'current-key',
90
+ private_key_pem: File.read('current_key.pem')
91
+ },
92
+ {
93
+ identifier: 'previous-key',
94
+ private_key_pem: File.read('previous_key.pem')
95
+ }
96
+ ]
97
+ )
98
+
99
+ # The library will try each key until decryption succeeds
100
+ decrypted_data = context.decrypt(token)
101
+ ```
102
+
103
+ ### Decrypted Data Structure
104
+
105
+ The decrypted payment data contains:
106
+
107
+ ```ruby
108
+ {
109
+ "gatewayMerchantId" => "your-gateway-merchant-id",
110
+ "messageExpiration" => "1234567890123",
111
+ "messageId" => "AH2Ejtc...",
112
+ "paymentMethod" => "CARD",
113
+ "paymentMethodDetails" => {
114
+ "pan" => "4111111111111111",
115
+ "expirationMonth" => 12,
116
+ "expirationYear" => 2025,
117
+ "authMethod" => "CRYPTOGRAM_3DS",
118
+ "cryptogram" => "AAAAAA...",
119
+ "eciIndicator" => "05"
120
+ }
121
+ }
122
+ ```
123
+
124
+ ## Error Handling
125
+
126
+ The gem raises `GooglePayRuby::GooglePaymentDecryptionError` when decryption fails:
127
+
128
+ ```ruby
129
+ begin
130
+ decrypted_data = context.decrypt(token)
131
+ rescue GooglePayRuby::GooglePaymentDecryptionError => e
132
+ # Access detailed error information
133
+ puts e.message
134
+ puts e.full_message # Includes details from all attempted keys
135
+
136
+ # Access individual errors
137
+ e.errors.each do |error|
138
+ puts "Merchant: #{error.merchant_identifier}"
139
+ puts "Error: #{error.message}"
140
+ end
141
+ end
142
+ ```
143
+
144
+ ## Development
145
+
146
+ After checking out the repo, run `bundle install` to install dependencies. Then, run `ruby test/test_decrypt.rb` to run the tests.
147
+
148
+ ## Contributing
149
+
150
+ Bug reports and pull requests are welcome on GitHub at https://github.com/better-payment/google-pay-ruby.
151
+
152
+ ## License
153
+
154
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
155
+
156
+ ## Credits
157
+
158
+ This Ruby implementation is inspired from [Basis Theory google-pay-js](https://github.com/Basis-Theory/google-pay-js) library.
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+ require 'base64'
5
+ require 'json'
6
+
7
+ module GooglePayRuby
8
+ class EcV2DecryptionStrategy
9
+ def initialize(private_key_pem)
10
+ @private_key = OpenSSL::PKey::EC.new(private_key_pem)
11
+ end
12
+
13
+ def decrypt(signed_message)
14
+ parsed_message = JSON.parse(signed_message)
15
+
16
+ ephemeral_public_key = parsed_message['ephemeralPublicKey']
17
+ encrypted_message = parsed_message['encryptedMessage']
18
+ tag = parsed_message['tag']
19
+
20
+ shared_key = get_shared_key(ephemeral_public_key)
21
+ derived_key = get_derived_key(ephemeral_public_key, shared_key)
22
+
23
+ symmetric_encryption_key = derived_key[0...64]
24
+ mac_key = derived_key[64..-1]
25
+
26
+ verify_message_hmac(mac_key, tag, encrypted_message)
27
+
28
+ decrypted_message = decrypt_message(encrypted_message, symmetric_encryption_key)
29
+
30
+ JSON.parse(decrypted_message)
31
+ end
32
+
33
+ private
34
+
35
+ def get_shared_key(ephemeral_public_key_b64)
36
+ # Decode the ephemeral public key from base64
37
+ ephemeral_public_key_bytes = Base64.strict_decode64(ephemeral_public_key_b64)
38
+
39
+ # Create an EC point object for the ephemeral public key
40
+ group = OpenSSL::PKey::EC::Group.new('prime256v1')
41
+ point = OpenSSL::PKey::EC::Point.new(group, OpenSSL::BN.new(ephemeral_public_key_bytes, 2))
42
+
43
+ # Compute ECDH shared secret directly from the point (OpenSSL 3.0 compatible)
44
+ shared_secret = @private_key.dh_compute_key(point)
45
+
46
+ shared_secret.unpack1('H*')
47
+ end
48
+
49
+ def get_derived_key(ephemeral_public_key, shared_key)
50
+ # Concatenate ephemeral public key and shared key
51
+ info = Base64.strict_decode64(ephemeral_public_key) + [shared_key].pack('H*')
52
+
53
+ # Use HKDF with SHA-256
54
+ salt = "\x00" * 32
55
+
56
+ # HKDF implementation
57
+ prk = OpenSSL::HMAC.digest('SHA256', salt, info)
58
+
59
+ # Expand with info="Google" to get 64 bytes
60
+ t = ''
61
+ okm = ''
62
+ counter = 1
63
+
64
+ while okm.length < 64
65
+ t = OpenSSL::HMAC.digest('SHA256', prk, t + 'Google' + [counter].pack('C'))
66
+ okm += t
67
+ counter += 1
68
+ end
69
+
70
+ okm[0...64].unpack1('H*')
71
+ end
72
+
73
+ def verify_message_hmac(mac_key, tag, encrypted_message)
74
+ mac_key_bytes = [mac_key].pack('H*')
75
+ encrypted_message_bytes = Base64.strict_decode64(encrypted_message)
76
+
77
+ calculated_hmac = OpenSSL::HMAC.digest('SHA256', mac_key_bytes, encrypted_message_bytes)
78
+ calculated_tag = Base64.strict_encode64(calculated_hmac)
79
+
80
+ unless calculated_tag == tag
81
+ raise GooglePaymentDecryptionError, 'Tag is not a valid MAC for the encrypted message'
82
+ end
83
+ end
84
+
85
+ def decrypt_message(encrypted_message, symmetric_encryption_key)
86
+ key = [symmetric_encryption_key].pack('H*')
87
+ iv = "\x00" * 16
88
+
89
+ encrypted_data = Base64.strict_decode64(encrypted_message)
90
+
91
+ decipher = OpenSSL::Cipher.new('AES-256-CTR')
92
+ decipher.decrypt
93
+ decipher.key = key
94
+ decipher.iv = iv
95
+
96
+ decrypted = decipher.update(encrypted_data) + decipher.final
97
+
98
+ decrypted.force_encoding('UTF-8')
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GooglePayRuby
4
+ class GooglePaymentDecryptionError < StandardError
5
+ attr_reader :errors
6
+
7
+ def initialize(message, errors = [])
8
+ @errors = errors
9
+ super(message)
10
+ end
11
+
12
+ def full_message
13
+ message_parts = [message]
14
+
15
+ if errors.any?
16
+ message_parts << "\nDecryption attempts failed:"
17
+ errors.each_with_index do |error, index|
18
+ merchant_id = error.respond_to?(:merchant_identifier) ? error.merchant_identifier : "Unknown"
19
+ message_parts << " [#{index + 1}] Merchant: #{merchant_id} - #{error.message}"
20
+ end
21
+ end
22
+
23
+ message_parts.join("\n")
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GooglePayRuby
4
+ class GooglePaymentMethodTokenContext
5
+ attr_reader :merchants
6
+
7
+ def initialize(options)
8
+ @merchants = options[:merchants] || []
9
+
10
+ if @merchants.empty?
11
+ raise GooglePaymentDecryptionError.new(
12
+ 'No merchant configuration provided for decryption context.'
13
+ )
14
+ end
15
+
16
+ validate_merchant_configurations!
17
+ end
18
+
19
+ def decrypt(token)
20
+ protocol_version = token['protocolVersion'] || token[:protocolVersion]
21
+ unless protocol_version == 'ECv2'
22
+ raise GooglePaymentDecryptionError.new(
23
+ "Unsupported decryption for protocol version #{protocol_version}"
24
+ )
25
+ end
26
+
27
+ errors = []
28
+ signed_message = token['signedMessage'] || token[:signedMessage]
29
+
30
+ @merchants.each do |merchant|
31
+ begin
32
+ strategy = EcV2DecryptionStrategy.new(merchant[:private_key_pem])
33
+ return strategy.decrypt(signed_message)
34
+ rescue StandardError => e
35
+ e.define_singleton_method(:merchant_identifier) { merchant[:identifier] }
36
+ errors << e
37
+ end
38
+ end
39
+
40
+ raise GooglePaymentDecryptionError.new(
41
+ 'Failed to decrypt payment data using provided merchant configuration(s).',
42
+ errors
43
+ )
44
+ end
45
+
46
+ private
47
+
48
+ def validate_merchant_configurations!
49
+ @merchants.each_with_index do |merchant, index|
50
+ unless merchant.is_a?(Hash)
51
+ raise ArgumentError, "Merchant configuration at index #{index} must be a Hash"
52
+ end
53
+
54
+ unless merchant[:private_key_pem]
55
+ raise ArgumentError, "Merchant configuration at index #{index} must include :private_key_pem"
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GooglePayRuby
4
+ VERSION = "1.0.0"
5
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "google_pay_ruby/version"
4
+ require_relative "google_pay_ruby/google_payment_method_token_context"
5
+ require_relative "google_pay_ruby/ec_v2_decryption_strategy"
6
+ require_relative "google_pay_ruby/google_payment_decryption_error"
7
+
8
+ module GooglePayRuby
9
+ class Error < StandardError; end
10
+ end
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: google_pay_ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Sahil Gadimbayli
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-02-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '13.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '13.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rubocop
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.21'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.21'
41
+ description: A Ruby implementation for securely decrypting Google Pay PaymentMethodTokens
42
+ using ECv2 protocol. Supports key rotation and multiple merchant configurations.
43
+ email:
44
+ - contact@sahilgadimbayli.com
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - CHANGELOG.md
50
+ - LICENSE.txt
51
+ - README.md
52
+ - lib/google_pay_ruby.rb
53
+ - lib/google_pay_ruby/ec_v2_decryption_strategy.rb
54
+ - lib/google_pay_ruby/google_payment_decryption_error.rb
55
+ - lib/google_pay_ruby/google_payment_method_token_context.rb
56
+ - lib/google_pay_ruby/version.rb
57
+ homepage: https://github.com/better-payment/google-pay-ruby
58
+ licenses:
59
+ - MIT
60
+ metadata:
61
+ homepage_uri: https://github.com/better-payment/google-pay-ruby
62
+ source_code_uri: https://github.com/better-payment/google-pay-ruby
63
+ changelog_uri: https://github.com/better-payment/google-pay-ruby/blob/main/CHANGELOG.md
64
+ post_install_message:
65
+ rdoc_options: []
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: 2.7.0
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ requirements: []
79
+ rubygems_version: 3.4.19
80
+ signing_key:
81
+ specification_version: 4
82
+ summary: Ruby utility for decrypting Google Pay Tokens
83
+ test_files: []