mastercard-client-encryption 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|