mastercard-client-encryption 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 +7 -0
- data/lib/mcapi/encryption/crypto/crypto.rb +174 -0
- data/lib/mcapi/encryption/field_level_encryption.rb +145 -0
- data/lib/mcapi/encryption/openapi_interceptor.rb +65 -0
- data/lib/mcapi/encryption/utils/hash.ext.rb +13 -0
- data/lib/mcapi/encryption/utils/openssl_rsa_oaep.rb +128 -0
- data/lib/mcapi/encryption/utils/utils.rb +144 -0
- metadata +105 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 1a98e0ce57b2212e9f27e9d09c2aa3586afa57c8f351549b1566607525608c2e
|
4
|
+
data.tar.gz: 4f054c956f369efc28dc7f288d39c0105769232084806379d1b4a07ff0200094
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c2efcd434d514d9404a583c22bf3417cdb988a407cd29c1ce84b85e577a11c6582c7b4c51eff4c09db21e104d08bcf79517838ac4d639b88ee4c9242b9e283d0
|
7
|
+
data.tar.gz: d3399d4cec5349ffc560e1f2de941f04bb9cdf5a75e2c897afaeefad648e415fcd22b1127a33fef3c0423715229061d9d3a260552b8ba51b852c090965f35a19
|
@@ -0,0 +1,174 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'openssl'
|
5
|
+
require 'base64'
|
6
|
+
require_relative '../utils/utils'
|
7
|
+
require_relative '../utils/openssl_rsa_oaep'
|
8
|
+
|
9
|
+
module McAPI
|
10
|
+
module Encryption
|
11
|
+
#
|
12
|
+
# Crypto class provide RSA/AES encrypt/decrypt methods
|
13
|
+
#
|
14
|
+
class Crypto
|
15
|
+
#
|
16
|
+
# Create a new instance with the provided config
|
17
|
+
#
|
18
|
+
# @param [Hash] config configuration object
|
19
|
+
#
|
20
|
+
def initialize(config)
|
21
|
+
valid_config?(config)
|
22
|
+
@encoding = config['dataEncoding']
|
23
|
+
@cert = OpenSSL::X509::Certificate.new(File.read(config['encryptionCertificate']))
|
24
|
+
if config['privateKey']
|
25
|
+
@private_key = OpenSSL::PKey.read(File.new(config['privateKey']))
|
26
|
+
elsif config['keyStore']
|
27
|
+
@private_key = OpenSSL::PKCS12.new(File.read(config['keyStore']), config['keyStorePassword']).key
|
28
|
+
end
|
29
|
+
@oaep_hashing_alg = config['oaepPaddingDigestAlgorithm']
|
30
|
+
@encrypted_value_field_name = config['encryptedValueFieldName']
|
31
|
+
@encrypted_key_field_name = config['encryptedKeyFieldName']
|
32
|
+
@public_key_fingerprint = compute_public_fingerprint(config['publicKeyFingerprintType'])
|
33
|
+
@public_key_fingerprint_field_name = config['publicKeyFingerprintFieldName']
|
34
|
+
@oaep_hashing_alg_field_name = config['oaepHashingAlgorithmFieldName']
|
35
|
+
end
|
36
|
+
|
37
|
+
#
|
38
|
+
# Generate encryption parameters.
|
39
|
+
#
|
40
|
+
# @param [String] iv IV to use instead to generate a random IV
|
41
|
+
# @param [String] secret_key Secret Key to use instead to generate a random key
|
42
|
+
#
|
43
|
+
# @return [Hash] hash with the generated encryption parameters
|
44
|
+
#
|
45
|
+
def new_encryption_params(iv = nil, secret_key = nil)
|
46
|
+
# Generate a secret key (should be 128 (or 256) bits)
|
47
|
+
secret_key ||= OpenSSL::Random.random_bytes(16)
|
48
|
+
# Generate a random initialization vector (IV)
|
49
|
+
iv ||= OpenSSL::Random.random_bytes(16)
|
50
|
+
md = Utils.create_message_digest(@oaep_hashing_alg)
|
51
|
+
# Encrypt secret key with issuer key
|
52
|
+
encrypted_key = @cert.public_key.public_encrypt_oaep(secret_key, '', md, md)
|
53
|
+
|
54
|
+
{
|
55
|
+
iv: iv,
|
56
|
+
secretKey: secret_key,
|
57
|
+
encryptedKey: encrypted_key,
|
58
|
+
oaepHashingAlgorithm: @oaep_hashing_alg,
|
59
|
+
publicKeyFingerprint: @public_key_fingerprint,
|
60
|
+
encoded: {
|
61
|
+
iv: Utils.encode(iv, @encoding),
|
62
|
+
secretKey: Utils.encode(secret_key, @encoding),
|
63
|
+
encryptedKey: Utils.encode(encrypted_key, @encoding)
|
64
|
+
}
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
#
|
69
|
+
# Perform data encryption:
|
70
|
+
# If +iv+, +secret_key+, +encryption_params+ and +encoding+ are not provided, randoms will be generated.
|
71
|
+
#
|
72
|
+
# @param [String] data json string to encrypt
|
73
|
+
# @param [String] (optional) iv Initialization vector to use to create the cipher, if not provided generate a random one
|
74
|
+
# @param [String] (optional) encryption_params encryption parameters
|
75
|
+
# @param [String] encoding encoding to use for the encrypted bytes (hex or base64)
|
76
|
+
#
|
77
|
+
# @return [String] encrypted data
|
78
|
+
#
|
79
|
+
def encrypt_data(data:, iv: nil, secret_key: nil, encryption_params: nil, encoding: nil)
|
80
|
+
encoding ||= @encoding
|
81
|
+
encryption_params ||= new_encryption_params(iv, secret_key)
|
82
|
+
# Create Symmetric Cipher: AES 128-bit
|
83
|
+
aes = OpenSSL::Cipher::AES.new(128, :CBC)
|
84
|
+
# Initialize for encryption mode
|
85
|
+
aes.encrypt
|
86
|
+
aes.iv = encryption_params[:iv]
|
87
|
+
aes.key = encryption_params[:secretKey]
|
88
|
+
encrypted = aes.update(data) + aes.final
|
89
|
+
data = {
|
90
|
+
@encrypted_value_field_name => Utils.encode(encrypted, encoding),
|
91
|
+
'iv' => Utils.encode(encryption_params[:iv], encoding)
|
92
|
+
}
|
93
|
+
data[@encrypted_key_field_name] = Utils.encode(encryption_params[:encryptedKey], encoding) if @encrypted_key_field_name
|
94
|
+
data[@public_key_fingerprint_field_name] = @public_key_fingerprint if @public_key_fingerprint
|
95
|
+
data[@oaep_hashing_alg_field_name] = @oaep_hashing_alg.sub('-', '') if @oaep_hashing_alg_field_name
|
96
|
+
data
|
97
|
+
end
|
98
|
+
|
99
|
+
#
|
100
|
+
# Perform data decryption
|
101
|
+
#
|
102
|
+
# @param [String] encrypted_data encrypted data to decrypt
|
103
|
+
# @param [String] iv Initialization vector to use to create the Decipher
|
104
|
+
# @param [String] encrypted_key Encrypted key to use to decrypt the data
|
105
|
+
# (the key is the decrypted using the provided PrivateKey)
|
106
|
+
#
|
107
|
+
# @return [String] Decrypted JSON object
|
108
|
+
#
|
109
|
+
def decrypt_data(encrypted_data, iv, encrypted_key)
|
110
|
+
md = Utils.create_message_digest(@oaep_hashing_alg)
|
111
|
+
decrypted_key = @private_key.private_decrypt_oaep(Utils.decode(encrypted_key, @encoding), '', md, md)
|
112
|
+
aes = OpenSSL::Cipher::AES.new(decrypted_key.size * 8, :CBC)
|
113
|
+
aes.decrypt
|
114
|
+
aes.key = decrypted_key
|
115
|
+
aes.iv = Utils.decode(iv, @encoding)
|
116
|
+
aes.update(Utils.decode(encrypted_data, @encoding)) + aes.final
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
#
|
122
|
+
# Compute the fingerprint for the provided public key
|
123
|
+
#
|
124
|
+
# @param [String] type: +certificate+ or +publickey+
|
125
|
+
#
|
126
|
+
# @return [String] the computed fingerprint encoded using the configured encoding
|
127
|
+
#
|
128
|
+
def compute_public_fingerprint(type)
|
129
|
+
return unless type
|
130
|
+
|
131
|
+
case type.downcase
|
132
|
+
when 'certificate'
|
133
|
+
if @encoding == 'hex'
|
134
|
+
OpenSSL::Digest::SHA256.new(@cert.to_der).to_s
|
135
|
+
else
|
136
|
+
Digest::SHA256.base64digest(@cert.to_der)
|
137
|
+
end
|
138
|
+
when 'publickey'
|
139
|
+
OpenSSL::Digest::SHA256.new(@cert.public_key.to_der).to_s
|
140
|
+
else
|
141
|
+
raise 'Selected public fingerprint not supported'
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
#
|
146
|
+
# Check if the passed configuration is valid
|
147
|
+
#
|
148
|
+
def valid_config?(config)
|
149
|
+
props_basic = %w[oaepPaddingDigestAlgorithm paths dataEncoding encryptionCertificate encryptedValueFieldName]
|
150
|
+
props_field = %w[ivFieldName encryptedKeyFieldName]
|
151
|
+
props_header = %w[ivHeaderName encryptedKeyHeaderName oaepHashingAlgorithmHeaderName]
|
152
|
+
props_fingerprint = %w[publicKeyFingerprintType publicKeyFingerprintFieldName publicKeyFingerprintHeaderName]
|
153
|
+
props_opt_fingerprint = %w[publicKeyFingerprint]
|
154
|
+
|
155
|
+
raise 'Config not valid: config should be an Hash.' unless config.is_a?(Hash)
|
156
|
+
raise 'Config not valid: paths should be an array of path element.' unless config['paths'] && config['paths'].is_a?(Array)
|
157
|
+
|
158
|
+
check_props = !Utils.contains(config, props_basic) ||
|
159
|
+
(!Utils.contains(config, props_field) && !Utils.contains(config, props_header))
|
160
|
+
raise 'Config not valid: please check that all the properties are defined.' if check_props
|
161
|
+
|
162
|
+
raise 'Config not valid: paths should be not empty.' if config['paths'].length.zero?
|
163
|
+
raise "Config not valid: dataEncoding should be 'hex' or 'base64'" if config['dataEncoding'] != 'hex' &&
|
164
|
+
config['dataEncoding'] != 'base64'
|
165
|
+
|
166
|
+
check_finger = !Utils.contains(config, props_opt_fingerprint) &&
|
167
|
+
(config[props_fingerprint[1]] || config[props_fingerprint[2]]) &&
|
168
|
+
config[props_fingerprint[0]] != 'certificate' &&
|
169
|
+
config[props_fingerprint[0]] != 'publicKey'
|
170
|
+
raise "Config not valid: propertiesFingerprint should be: 'certificate' or 'publicKey'" if check_finger
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'crypto/crypto'
|
4
|
+
require_relative 'utils/hash.ext'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
module McAPI
|
8
|
+
module Encryption
|
9
|
+
#
|
10
|
+
# Performs field level encryption on HTTP payloads.
|
11
|
+
#
|
12
|
+
class FieldLevelEncryption
|
13
|
+
#
|
14
|
+
# Create a new instance with the provided configuration
|
15
|
+
#
|
16
|
+
# @param [Object] config Configuration object
|
17
|
+
#
|
18
|
+
def initialize(config)
|
19
|
+
@config = config
|
20
|
+
@crypto = McAPI::Encryption::Crypto.new(config)
|
21
|
+
@is_with_header = config['ivHeaderName'] && config['encryptedKeyHeaderName']
|
22
|
+
@encryption_response_properties = [@config['ivFieldName'], @config['encryptedKeyFieldName'],
|
23
|
+
@config['publicKeyFingerprintFieldName'], @config['oaepHashingAlgorithmFieldName']]
|
24
|
+
end
|
25
|
+
|
26
|
+
#
|
27
|
+
# Encrypt parts of a HTTP request using the given config
|
28
|
+
#
|
29
|
+
# @param [String] endpoint HTTP URL for the current call
|
30
|
+
# @param [Object] header HTTP header
|
31
|
+
# @param [String,Hash] body HTTP body
|
32
|
+
#
|
33
|
+
# @return [Hash] Hash with two keys:
|
34
|
+
# * :header header with encrypted value (if configured with header)
|
35
|
+
# * :body encrypted body
|
36
|
+
#
|
37
|
+
def encrypt(endpoint, header, body)
|
38
|
+
body = JSON.parse(body) if body.is_a?(String)
|
39
|
+
config = config?(endpoint)
|
40
|
+
if config
|
41
|
+
if !@is_with_header
|
42
|
+
config['toEncrypt'].each do |v|
|
43
|
+
encrypt_with_body(v, body)
|
44
|
+
end
|
45
|
+
else
|
46
|
+
enc_params = @crypto.new_encryption_params
|
47
|
+
config['toEncrypt'].each do |v|
|
48
|
+
body = encrypt_with_header(v, enc_params, header, body)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
{ header: header, body: body.json }
|
53
|
+
end
|
54
|
+
|
55
|
+
#
|
56
|
+
# Decrypt part of the HTTP response using the given config
|
57
|
+
#
|
58
|
+
# @param [Object] response object as obtained from the http client
|
59
|
+
#
|
60
|
+
# @return [Object] response object with decrypted fields
|
61
|
+
#
|
62
|
+
def decrypt(response)
|
63
|
+
response = JSON.parse(response)
|
64
|
+
config = config?(response['request']['url'])
|
65
|
+
if config
|
66
|
+
if !@is_with_header
|
67
|
+
config['toDecrypt'].each do |v|
|
68
|
+
decrypt_with_body(v, response['body'])
|
69
|
+
end
|
70
|
+
else
|
71
|
+
config['toDecrypt'].each do |v|
|
72
|
+
elem = elem_from_path(v['obj'], response['body'])
|
73
|
+
decrypt_with_header(v, elem, response) if elem[:node][v['element']]
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
JSON.generate(response)
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def encrypt_with_body(path, body)
|
83
|
+
elem = elem_from_path(path['element'], body)
|
84
|
+
encrypted_data = @crypto.encrypt_data(data: JSON.generate(elem[:node]))
|
85
|
+
McAPI::Utils.mutate_obj_prop(path['obj'], encrypted_data, body)
|
86
|
+
McAPI::Utils.delete_node(path['element'], body) if path['element'] != "#{path['obj']}.#{@config['encryptedValueFieldName']}"
|
87
|
+
end
|
88
|
+
|
89
|
+
def encrypt_with_header(path, enc_params, header, body)
|
90
|
+
elem = elem_from_path(path['element'], body)
|
91
|
+
encrypted_data = @crypto.encrypt_data(data: JSON.generate(elem[:node]), encryption_params: enc_params)
|
92
|
+
body = { path['obj'] => { @config['encryptedValueFieldName'] => encrypted_data[@config['encryptedValueFieldName']] } }
|
93
|
+
set_header(header, enc_params)
|
94
|
+
body
|
95
|
+
end
|
96
|
+
|
97
|
+
def decrypt_with_body(path, body)
|
98
|
+
elem = elem_from_path(path['element'], body)
|
99
|
+
return unless elem && elem[:node]
|
100
|
+
|
101
|
+
decrypted = JSON.parse(@crypto.decrypt_data(elem[:node][@config['encryptedValueFieldName']],
|
102
|
+
elem[:node][@config['ivFieldName']],
|
103
|
+
elem[:node][@config['encryptedKeyFieldName']]))
|
104
|
+
McAPI::Utils.mutate_obj_prop(path['obj'], decrypted, body, path['element'], @encryption_response_properties)
|
105
|
+
end
|
106
|
+
|
107
|
+
def decrypt_with_header(path, elem, response)
|
108
|
+
encrypted_data = elem[:node][path['element']][@config['encryptedValueFieldName']]
|
109
|
+
response['body'].clear
|
110
|
+
response['body'] = JSON.parse(@crypto.decrypt_data(encrypted_data,
|
111
|
+
response['headers'][@config['ivHeaderName']][0],
|
112
|
+
response['headers'][@config['encryptedKeyHeaderName']][0]))
|
113
|
+
end
|
114
|
+
|
115
|
+
def elem_from_path(path, obj)
|
116
|
+
parent = nil
|
117
|
+
paths = path.split('.')
|
118
|
+
if path && !paths.empty?
|
119
|
+
paths.each do |e|
|
120
|
+
parent = obj
|
121
|
+
obj = obj[e]
|
122
|
+
end
|
123
|
+
end
|
124
|
+
{ node: obj, parent: parent }
|
125
|
+
rescue StandardError
|
126
|
+
nil
|
127
|
+
end
|
128
|
+
|
129
|
+
def config?(endpoint)
|
130
|
+
return unless endpoint
|
131
|
+
|
132
|
+
endpoint = endpoint.split('?').shift
|
133
|
+
conf = @config['paths'].select { |e| endpoint.match(e['path']) }
|
134
|
+
conf.empty? ? nil : conf[0]
|
135
|
+
end
|
136
|
+
|
137
|
+
def set_header(header, params)
|
138
|
+
header[@config['encryptedKeyHeaderName']] = params[:encoded][:encryptedKey]
|
139
|
+
header[@config['ivHeaderName']] = params[:encoded][:iv]
|
140
|
+
header[@config['oaepHashingAlgorithmHeaderName']] = params[:oaepHashingAlgorithm].sub('-', '')
|
141
|
+
header[@config['publicKeyFingerprintHeaderName']] = params[:publicKeyFingerprint]
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'field_level_encryption'
|
4
|
+
require_relative 'utils/utils'
|
5
|
+
|
6
|
+
module McAPI
|
7
|
+
module Encryption
|
8
|
+
##
|
9
|
+
# Service class that provide interceptor facilities for OpenApi swagger client
|
10
|
+
#
|
11
|
+
class OpenAPIInterceptor
|
12
|
+
class << self
|
13
|
+
#
|
14
|
+
# Install the field level encryption in the OpenAPI HTTP client
|
15
|
+
# adding encryption/decryption capabilities for the request/response payload.
|
16
|
+
#
|
17
|
+
# @param [Object] swagger_client OpenAPI service client (it can be generated by the swagger code generator)
|
18
|
+
# @param [Object] config configuration object describing which field to enable encryption/decryption
|
19
|
+
#
|
20
|
+
def install_field_level_encryption(swagger_client, config)
|
21
|
+
fle = McAPI::Encryption::FieldLevelEncryption.new(config)
|
22
|
+
# Hooking ApiClient#call_api
|
23
|
+
hook_call_api fle
|
24
|
+
# Hooking ApiClient#deserialize
|
25
|
+
hook_deserialize fle
|
26
|
+
McAPI::Encryption::OpenAPIInterceptor.init_call_api swagger_client
|
27
|
+
McAPI::Encryption::OpenAPIInterceptor.init_deserialize swagger_client
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def hook_call_api(fle)
|
33
|
+
self.class.send :define_method, :init_call_api do |client|
|
34
|
+
client.define_singleton_method(:call_api) do |http_method, path, opts|
|
35
|
+
if opts && opts[:body]
|
36
|
+
encrypted = fle.encrypt(path, opts[:header_params], opts[:body])
|
37
|
+
opts[:body] = JSON.generate(encrypted[:body])
|
38
|
+
end
|
39
|
+
# noinspection RubySuperCallWithoutSuperclassInspection
|
40
|
+
super(http_method, path, opts)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def hook_deserialize(fle)
|
46
|
+
self.class.send :define_method, :init_deserialize do |client|
|
47
|
+
client.define_singleton_method(:deserialize) do |response, return_type|
|
48
|
+
if response && response.body
|
49
|
+
endpoint = response.request.base_url.sub client.config.base_url, ''
|
50
|
+
to_decrypt = { headers: McAPI::Utils.parse_header(response.options[:response_headers]),
|
51
|
+
request: { url: endpoint },
|
52
|
+
body: JSON.parse(response.body) }
|
53
|
+
decrypted = fle.decrypt(JSON.generate(to_decrypt, symbolize_names: false))
|
54
|
+
body = JSON.generate(JSON.parse(decrypted)['body'])
|
55
|
+
response.options[:response_body] = JSON.generate(JSON.parse(body))
|
56
|
+
end
|
57
|
+
# noinspection RubySuperCallWithoutSuperclassInspection
|
58
|
+
super(response, return_type)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'openssl'
|
4
|
+
|
5
|
+
#
|
6
|
+
# Extends the OpenSSL library with RSA OAEP MGF1 padding
|
7
|
+
#
|
8
|
+
|
9
|
+
module OpenSSL
|
10
|
+
module PKey
|
11
|
+
class RSA
|
12
|
+
def public_encrypt_oaep(str, label = '', md = nil, mgf1md = nil)
|
13
|
+
padded = PKCS1.add_oaep_mgf1(str, n.num_bytes, label, md, mgf1md)
|
14
|
+
public_encrypt(padded, OpenSSL::PKey::RSA::NO_PADDING)
|
15
|
+
end
|
16
|
+
|
17
|
+
def private_decrypt_oaep(str, label = '', md = nil, mgf1md = nil)
|
18
|
+
padded = private_decrypt(str, OpenSSL::PKey::RSA::NO_PADDING)
|
19
|
+
PKCS1.check_oaep_mgf1(padded, label, md, mgf1md)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module PKCS1
|
25
|
+
def add_oaep_mgf1(str, len, label = '', md = nil, mgf1md = nil)
|
26
|
+
md ||= OpenSSL::Digest::SHA1
|
27
|
+
mgf1md ||= md
|
28
|
+
|
29
|
+
mdlen = md.new.digest_length
|
30
|
+
z_len = len - str.bytesize - 2 * mdlen - 2
|
31
|
+
raise OpenSSL::PKey::RSAError, 'key size too small' if len < 2 * mdlen + 1
|
32
|
+
raise OpenSSL::PKey::RSAError, 'data too large for key size' if z_len.negative?
|
33
|
+
|
34
|
+
l_hash = md.digest(label)
|
35
|
+
db = l_hash + ([0] * z_len + [1]).pack('C*') + [str].pack('a*')
|
36
|
+
seed = OpenSSL::Random.random_bytes(mdlen)
|
37
|
+
|
38
|
+
masked_db = mgf1_xor(db, seed, mgf1md)
|
39
|
+
masked_seed = mgf1_xor(seed, masked_db, mgf1md)
|
40
|
+
|
41
|
+
[0, masked_seed, masked_db].pack('Ca*a*')
|
42
|
+
end
|
43
|
+
|
44
|
+
module_function :add_oaep_mgf1
|
45
|
+
|
46
|
+
def check_oaep_mgf1(str, label = '', md = nil, mgf1md = nil)
|
47
|
+
md ||= OpenSSL::Digest::SHA1
|
48
|
+
mgf1md ||= md
|
49
|
+
|
50
|
+
mdlen = md.new.digest_length
|
51
|
+
em = str.bytes
|
52
|
+
raise OpenSSL::PKey::RSAError if em.size < 2 * mdlen + 2
|
53
|
+
|
54
|
+
# Keep constant calculation even if the text is invaid in order to avoid attacks.
|
55
|
+
good = secure_byte_is_zero(em[0])
|
56
|
+
masked_seed = em[1...1 + mdlen].pack('C*')
|
57
|
+
masked_db = em[1 + mdlen...em.size].pack('C*')
|
58
|
+
|
59
|
+
seed = mgf1_xor(masked_seed, masked_db, mgf1md)
|
60
|
+
db = mgf1_xor(masked_db, seed, mgf1md)
|
61
|
+
db_bytes = db.bytes
|
62
|
+
|
63
|
+
l_hash = md.digest(label)
|
64
|
+
good &= secure_hash_eq(l_hash.bytes, db_bytes[0...mdlen])
|
65
|
+
|
66
|
+
one_index = 0
|
67
|
+
found_one_byte = 0
|
68
|
+
(mdlen...db_bytes.size).each do |i|
|
69
|
+
equals1 = secure_byte_eq(db_bytes[i], 1)
|
70
|
+
equals0 = secure_byte_is_zero(db_bytes[i])
|
71
|
+
one_index = secure_select(~found_one_byte & equals1, i, one_index)
|
72
|
+
found_one_byte |= equals1
|
73
|
+
good &= (found_one_byte | equals0)
|
74
|
+
end
|
75
|
+
|
76
|
+
good &= found_one_byte
|
77
|
+
|
78
|
+
raise OpenSSL::PKey::RSAError if good.zero?
|
79
|
+
|
80
|
+
db_bytes[one_index + 1...db_bytes.size].pack('C*')
|
81
|
+
end
|
82
|
+
|
83
|
+
module_function :check_oaep_mgf1
|
84
|
+
|
85
|
+
def mgf1_xor(out, seed, md)
|
86
|
+
counter = 0
|
87
|
+
out_bytes = out.bytes
|
88
|
+
mask_bytes = []
|
89
|
+
while mask_bytes.size < out_bytes.size
|
90
|
+
mask_bytes += md.digest([seed, counter].pack('a*N')).bytes
|
91
|
+
counter += 1
|
92
|
+
end
|
93
|
+
out_bytes.size.times do |i|
|
94
|
+
out_bytes[i] ^= mask_bytes[i]
|
95
|
+
end
|
96
|
+
out_bytes.pack('C*')
|
97
|
+
end
|
98
|
+
|
99
|
+
module_function :mgf1_xor
|
100
|
+
|
101
|
+
# Constant time comparistion utilities.
|
102
|
+
def secure_byte_is_zero(v)
|
103
|
+
v - 1 >> 8
|
104
|
+
end
|
105
|
+
|
106
|
+
def secure_byte_eq(v1, v2)
|
107
|
+
secure_byte_is_zero(v1 ^ v2)
|
108
|
+
end
|
109
|
+
|
110
|
+
def secure_select(mask, eq, ne)
|
111
|
+
(mask & eq) | (~mask & ne)
|
112
|
+
end
|
113
|
+
|
114
|
+
def secure_hash_eq(vs1, vs2)
|
115
|
+
# Assumes the given hash values have the same size.
|
116
|
+
# This check is not constant time, but should not depends on the texts.
|
117
|
+
return 0 unless vs1.size == vs2.size
|
118
|
+
|
119
|
+
res = secure_byte_is_zero(0)
|
120
|
+
(0...vs1.size).each do |i|
|
121
|
+
res &= secure_byte_eq(vs1[i], vs2[i])
|
122
|
+
end
|
123
|
+
res
|
124
|
+
end
|
125
|
+
|
126
|
+
module_function :secure_byte_is_zero, :secure_byte_eq, :secure_select, :secure_hash_eq
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'base64'
|
4
|
+
|
5
|
+
module McAPI
|
6
|
+
#
|
7
|
+
# Utils module
|
8
|
+
module Utils
|
9
|
+
#
|
10
|
+
# Data encoding
|
11
|
+
#
|
12
|
+
def self.encode(data, encoding)
|
13
|
+
return unless encoding
|
14
|
+
|
15
|
+
case encoding.downcase
|
16
|
+
when 'hex'
|
17
|
+
data.each_byte.map { |b| format('%02x', b.to_i) }.join
|
18
|
+
when 'base64'
|
19
|
+
Base64.encode64(data).delete("\n")
|
20
|
+
else
|
21
|
+
raise 'Encoding not supported'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
#
|
26
|
+
# Data decoding
|
27
|
+
#
|
28
|
+
def self.decode(data, encoding)
|
29
|
+
return unless encoding
|
30
|
+
|
31
|
+
case encoding.downcase
|
32
|
+
when 'hex'
|
33
|
+
[data].pack('H*')
|
34
|
+
when 'base64'
|
35
|
+
Base64.decode64(data)
|
36
|
+
else
|
37
|
+
raise 'Encoding not supported'
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
#
|
42
|
+
# Create Digest object for the provided digest string
|
43
|
+
#
|
44
|
+
def self.create_message_digest(digest)
|
45
|
+
return unless digest
|
46
|
+
|
47
|
+
case digest.upcase
|
48
|
+
when 'SHA-256', 'SHA256'
|
49
|
+
OpenSSL::Digest::SHA256
|
50
|
+
when 'SHA-512', 'SHA512'
|
51
|
+
OpenSSL::Digest::SHA512
|
52
|
+
else
|
53
|
+
raise 'Digest algorithm not supported'
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.contains(config, props)
|
58
|
+
props.any? do |i|
|
59
|
+
config.key? i
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
#
|
64
|
+
# Perform JSON object properties manipulations
|
65
|
+
#
|
66
|
+
def self.mutate_obj_prop(path, value, obj, src_path = nil, properties = [])
|
67
|
+
tmp = obj
|
68
|
+
prev = nil
|
69
|
+
return unless path
|
70
|
+
|
71
|
+
delete_node(src_path, obj, properties) if src_path
|
72
|
+
paths = path.split('.')
|
73
|
+
paths.each do |e|
|
74
|
+
tmp[e] = {} unless tmp[e]
|
75
|
+
prev = tmp
|
76
|
+
tmp = tmp[e]
|
77
|
+
end
|
78
|
+
elem = path.split('.').pop
|
79
|
+
if value.is_a?(Hash) && !value.is_a?(Array)
|
80
|
+
prev[elem] = {} unless prev[elem].is_a?(Hash)
|
81
|
+
override_props(prev[elem], value)
|
82
|
+
else
|
83
|
+
prev[elem] = value
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.override_props(target, obj)
|
88
|
+
obj.each do |k, _|
|
89
|
+
target[k] = obj[k]
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
#
|
94
|
+
# Delete node from JSON object
|
95
|
+
#
|
96
|
+
def self.delete_node(path, obj, properties = [])
|
97
|
+
return unless path && obj
|
98
|
+
|
99
|
+
paths = path.split('.')
|
100
|
+
to_delete = paths[paths.size - 1]
|
101
|
+
paths.each_with_index do |e, index|
|
102
|
+
prev = obj
|
103
|
+
next unless obj[e]
|
104
|
+
|
105
|
+
obj = obj[e]
|
106
|
+
prev.delete(to_delete) if obj && index == paths.size - 1
|
107
|
+
end
|
108
|
+
properties.each { |e| obj.delete(e) } if paths.empty?
|
109
|
+
end
|
110
|
+
|
111
|
+
#
|
112
|
+
# Parse raw HTTP Header
|
113
|
+
#
|
114
|
+
def self.parse_header(raw)
|
115
|
+
raw = raw.partition("\n").last
|
116
|
+
header = Hash.new([].freeze)
|
117
|
+
field = nil
|
118
|
+
raw.each_line do |line|
|
119
|
+
case line
|
120
|
+
when /^([A-Za-z0-9!\#$%&'*+\-.^_`|~]+):\s*(.*?)\s*\z/om
|
121
|
+
field = Regexp.last_match(1)
|
122
|
+
value = Regexp.last_match(2)
|
123
|
+
field.downcase!
|
124
|
+
header[field] = [] unless header.key?(field)
|
125
|
+
header[field] << value
|
126
|
+
when /^\s+(.*?)\s*\z/om
|
127
|
+
value = Regexp.last_match(1)
|
128
|
+
raise Exception, "bad header '#{line}'." unless field
|
129
|
+
|
130
|
+
header[field][-1] << ' ' << value
|
131
|
+
else
|
132
|
+
raise Exception, "bad header '#{line}'."
|
133
|
+
end
|
134
|
+
end
|
135
|
+
header.each do |_key, values|
|
136
|
+
values.each do |value|
|
137
|
+
value.strip!
|
138
|
+
value.gsub!(/\s+/, ' ')
|
139
|
+
end
|
140
|
+
end
|
141
|
+
header
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
metadata
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mastercard-client-encryption
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mastercard
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-05-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.5'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.5'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: minitest
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '5.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '5.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: simplecov
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.16.1
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.16.1
|
69
|
+
description: Library for Mastercard API compliant payload encryption/decryption.
|
70
|
+
email:
|
71
|
+
executables: []
|
72
|
+
extensions: []
|
73
|
+
extra_rdoc_files: []
|
74
|
+
files:
|
75
|
+
- lib/mcapi/encryption/crypto/crypto.rb
|
76
|
+
- lib/mcapi/encryption/field_level_encryption.rb
|
77
|
+
- lib/mcapi/encryption/openapi_interceptor.rb
|
78
|
+
- lib/mcapi/encryption/utils/hash.ext.rb
|
79
|
+
- lib/mcapi/encryption/utils/openssl_rsa_oaep.rb
|
80
|
+
- lib/mcapi/encryption/utils/utils.rb
|
81
|
+
homepage: https://github.com/Mastercard/client-encryption-ruby
|
82
|
+
licenses:
|
83
|
+
- MIT
|
84
|
+
metadata: {}
|
85
|
+
post_install_message:
|
86
|
+
rdoc_options: []
|
87
|
+
require_paths:
|
88
|
+
- lib
|
89
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - ">="
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
requirements: []
|
100
|
+
rubyforge_project:
|
101
|
+
rubygems_version: 2.7.7
|
102
|
+
signing_key:
|
103
|
+
specification_version: 4
|
104
|
+
summary: Mastercard encryption library
|
105
|
+
test_files: []
|