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.
@@ -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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Hash extension
5
+ #
6
+ class Hash
7
+ #
8
+ # Parse the current hash as json
9
+ #
10
+ def json
11
+ JSON.parse(to_json)
12
+ end
13
+ 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: []