mastercard-client-encryption 1.0.2 → 1.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6ee1474b23b7bec011ba28254f5771a3ed89dd5995e4eed541d8382833d97429
4
- data.tar.gz: 6fd5ef2ca4c9094744175dcdf4e297ea4f4cfd9ff4b22f32c6dffda8ebd46c37
3
+ metadata.gz: 498fe9a62ff880357316d0dd97d78d27dae85bca253702d905e2769087579ba2
4
+ data.tar.gz: 22a35a9afebde5f8f746067154c339ced42edd4b0d4ec0d6fa920963f1de5abd
5
5
  SHA512:
6
- metadata.gz: 32064f9cf37511cc6d25e9922f25683ed522f472830ba1570212cc42c1e1cfb72c97f405114937b124d83ad8717eca828d4933089ab2ec266b411e6ba9816a71
7
- data.tar.gz: aba8ffdc24a32e4af8cef5c9c3417b94e72922ce10a290cc026e4e9275a7e7ea92ed3787279cda55e966f956d1e1e157559f7142bdf4c5f6b5592986b1367254
6
+ metadata.gz: 3fe63908d6249e94fc6db683aefe97a82622ee4c8c59eca8e8a7cd21f7d79a96c0476f6efb6bc9362d2903eed8afc0a0d04a37b2b8a44ed438989560c5b47034
7
+ data.tar.gz: 879b24f693402374293b543d7a5bbacd4b05b12b23f4586a85ee2c078a3631fa16159051cc2acac2f07820fa48ddfdf6f1205dcd3419cf9cd2d6851af0ebb383
@@ -20,11 +20,11 @@ module McAPI
20
20
  def initialize(config)
21
21
  valid_config?(config)
22
22
  @encoding = config['dataEncoding']
23
- @cert = OpenSSL::X509::Certificate.new(File.read(config['encryptionCertificate']))
23
+ @cert = OpenSSL::X509::Certificate.new(IO.binread(config['encryptionCertificate']))
24
24
  if config['privateKey']
25
- @private_key = OpenSSL::PKey.read(File.new(config['privateKey']))
25
+ @private_key = OpenSSL::PKey.read(IO.binread(config['privateKey']))
26
26
  elsif config['keyStore']
27
- @private_key = OpenSSL::PKCS12.new(File.read(config['keyStore']), config['keyStorePassword']).key
27
+ @private_key = OpenSSL::PKCS12.new(IO.binread(config['keyStore']), config['keyStorePassword']).key
28
28
  end
29
29
  @oaep_hashing_alg = config['oaepPaddingDigestAlgorithm']
30
30
  @encrypted_value_field_name = config['encryptedValueFieldName']
@@ -37,8 +37,8 @@ module McAPI
37
37
  #
38
38
  # Generate encryption parameters.
39
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
40
+ # @param [String,nil] iv IV to use instead to generate a random IV
41
+ # @param [String,nil] secret_key Secret Key to use instead to generate a random key
42
42
  #
43
43
  # @return [Hash] hash with the generated encryption parameters
44
44
  #
@@ -70,8 +70,8 @@ module McAPI
70
70
  # If +iv+, +secret_key+, +encryption_params+ and +encoding+ are not provided, randoms will be generated.
71
71
  #
72
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
73
+ # @param [String,nil] (optional) iv Initialization vector to use to create the cipher, if not provided generate a random one
74
+ # @param [String,nil] (optional) encryption_params encryption parameters
75
75
  # @param [String] encoding encoding to use for the encrypted bytes (hex or base64)
76
76
  #
77
77
  # @return [String] encrypted data
@@ -103,11 +103,12 @@ module McAPI
103
103
  # @param [String] iv Initialization vector to use to create the Decipher
104
104
  # @param [String] encrypted_key Encrypted key to use to decrypt the data
105
105
  # (the key is the decrypted using the provided PrivateKey)
106
+ # @param [String] oaep_hashing_alg OAEP Algorithm to use
106
107
  #
107
108
  # @return [String] Decrypted JSON object
108
109
  #
109
- def decrypt_data(encrypted_data, iv, encrypted_key)
110
- md = Utils.create_message_digest(@oaep_hashing_alg)
110
+ def decrypt_data(encrypted_data, iv, encrypted_key, oaep_hashing_alg)
111
+ md = Utils.create_message_digest(oaep_hashing_alg)
111
112
  decrypted_key = @private_key.private_decrypt_oaep(Utils.decode(encrypted_key, @encoding), '', md, md)
112
113
  aes = OpenSSL::Cipher::AES.new(decrypted_key.size * 8, :CBC)
113
114
  aes.decrypt
@@ -121,7 +122,7 @@ module McAPI
121
122
  #
122
123
  # Compute the fingerprint for the provided public key
123
124
  #
124
- # @param [String] type: +certificate+ or +publickey+
125
+ # @param [Hash] type: +certificate+ or +publickey+
125
126
  #
126
127
  # @return [String] the computed fingerprint encoded using the configured encoding
127
128
  #
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'openssl'
5
+ require 'base64'
6
+ require 'securerandom'
7
+ require_relative '../utils/utils'
8
+ require_relative '../utils/openssl_rsa_oaep'
9
+
10
+ module McAPI
11
+ module Encryption
12
+ #
13
+ # JWE Crypto class provide RSA/AES encrypt/decrypt methods
14
+ #
15
+ class JweCrypto
16
+ #
17
+ # Create a new instance with the provided config
18
+ #
19
+ # @param [Hash] config configuration object
20
+ #
21
+ def initialize(config)
22
+ @encoding = config['dataEncoding']
23
+ @cert = OpenSSL::X509::Certificate.new(IO.binread(config['encryptionCertificate']))
24
+ if config['privateKey']
25
+ @private_key = OpenSSL::PKey.read(IO.binread(config['privateKey']))
26
+ elsif config['keyStore']
27
+ @private_key = OpenSSL::PKCS12.new(IO.binread(config['keyStore']), config['keyStorePassword']).key
28
+ end
29
+ @encrypted_value_field_name = config['encryptedValueFieldName'] || 'encryptedData'
30
+ @public_key_fingerprint = compute_public_fingerprint
31
+ end
32
+
33
+ #
34
+ # Perform data encryption:
35
+ #
36
+ # @param [String] data json string to encrypt
37
+ #
38
+ # @return [Hash] encrypted data
39
+ #
40
+ def encrypt_data(data:)
41
+ cek = SecureRandom.random_bytes(32)
42
+ iv = SecureRandom.random_bytes(12)
43
+
44
+ md = OpenSSL::Digest::SHA256
45
+ encrypted_key = @cert.public_key.public_encrypt_oaep(cek, '', md, md)
46
+
47
+ header = generate_header('RSA-OAEP-256', 'A256GCM')
48
+ json_hdr = header.to_json
49
+ auth_data = jwe_encode(json_hdr)
50
+
51
+ cipher = OpenSSL::Cipher.new('aes-256-gcm')
52
+ cipher.encrypt
53
+ cipher.key = cek
54
+ cipher.iv = iv
55
+ cipher.padding = 0
56
+ cipher.auth_data = auth_data
57
+ cipher_text = cipher.update(data) + cipher.final
58
+
59
+ payload = generate_serialization(json_hdr, encrypted_key, cipher_text, iv, cipher.auth_tag)
60
+ {
61
+ @encrypted_value_field_name => payload
62
+ }
63
+ end
64
+
65
+ #
66
+ # Perform data decryption
67
+ #
68
+ # @param [String] encrypted_data encrypted data to decrypt
69
+ #
70
+ # @return [String] Decrypted JSON object
71
+ #
72
+ def decrypt_data(encrypted_data:)
73
+ parts = encrypted_data.split('.')
74
+ encrypted_header, encrypted_key, initialization_vector, cipher_text, authentication_tag = parts
75
+
76
+ jwe_header = jwe_decode(encrypted_header)
77
+ encrypted_key = jwe_decode(encrypted_key)
78
+ iv = jwe_decode(initialization_vector)
79
+ cipher_text = jwe_decode(cipher_text)
80
+ cipher_tag = jwe_decode(authentication_tag)
81
+
82
+ md = OpenSSL::Digest::SHA256
83
+ cek = @private_key.private_decrypt_oaep(encrypted_key, '', md, md)
84
+
85
+ enc_method = JSON.parse(jwe_header)['enc']
86
+
87
+ if enc_method == "A256GCM"
88
+ enc_string = "aes-256-gcm"
89
+ elsif enc_method == "A128CBC-HS256"
90
+ cek = cek.byteslice(16, cek.length)
91
+ enc_string = "aes-128-cbc"
92
+ else
93
+ raise Exception, "Encryption method '#{enc_method}' not supported."
94
+ end
95
+
96
+ cipher = OpenSSL::Cipher.new(enc_string)
97
+ cipher.decrypt
98
+ cipher.key = cek
99
+ cipher.iv = iv
100
+ cipher.padding = 0
101
+ if enc_method == "A256GCM"
102
+ cipher.auth_data = encrypted_header
103
+ cipher.auth_tag = cipher_tag
104
+ end
105
+
106
+ cipher.update(cipher_text) + cipher.final
107
+ end
108
+
109
+ private
110
+
111
+ #
112
+ # Compute the fingerprint for the provided public key
113
+ #
114
+ # @return [String] the computed fingerprint encoded using the configured encoding
115
+ #
116
+ def compute_public_fingerprint
117
+ OpenSSL::Digest::SHA256.new(@cert.public_key.to_der).to_s
118
+ end
119
+
120
+ #
121
+ # Generate the JWE header for the provided encryption algorithm and encryption method
122
+ #
123
+ # @param [String] alg the cryptographic algorithm used to encrypt the value of the CEK
124
+ # @param [String] enc the content encryption algorithm used to perform authenticated encryption on the plaintext
125
+ #
126
+ # @return [Hash] the JWE header
127
+ #
128
+ def generate_header(alg, enc)
129
+ { alg: alg, enc: enc, kid: @public_key_fingerprint, cty: 'application/json' }
130
+ end
131
+
132
+ #
133
+ # URL safe Base64 encode the provided value
134
+ #
135
+ # @param [String] value to be encoded
136
+ #
137
+ # @return [String] URL safe Base64 encoded value
138
+ #
139
+ def jwe_encode(value)
140
+ ::Base64.urlsafe_encode64(value).delete('=')
141
+ end
142
+
143
+ #
144
+ # URL safe Base64 decode the provided value
145
+ #
146
+ # @param [String] value to be decoded
147
+ #
148
+ # @return [String] URL safe Base64 decoded value
149
+ #
150
+ def jwe_decode(value)
151
+ padlen = 4 - (value.length % 4)
152
+ if padlen < 4
153
+ pad = '=' * padlen
154
+ value += pad
155
+ end
156
+ ::Base64.urlsafe_decode64(value)
157
+ end
158
+
159
+ #
160
+ # Generate JWE compact payload from the provided values
161
+ #
162
+ # @param [String] hdr JWE header
163
+ # @param [String] cek content encryption key
164
+ # @param [String] content cipher text
165
+ # @param [String] iv initialization vector
166
+ # @param [String] tag cipher auth tag
167
+ #
168
+ # @return [String] URL safe Base64 decoded value
169
+ #
170
+ def generate_serialization(hdr, cek, content, iv, tag)
171
+ [hdr, cek, iv, content, tag].map { |piece| jwe_encode(piece) }.join '.'
172
+ end
173
+ end
174
+ end
175
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative 'crypto/crypto'
4
4
  require_relative 'utils/hash.ext'
5
+ require_relative 'utils/utils'
5
6
  require 'json'
6
7
 
7
8
  module McAPI
@@ -13,7 +14,7 @@ module McAPI
13
14
  #
14
15
  # Create a new instance with the provided configuration
15
16
  #
16
- # @param [Object] config Configuration object
17
+ # @param [Hash] config Configuration object
17
18
  #
18
19
  def initialize(config)
19
20
  @config = config
@@ -27,7 +28,7 @@ module McAPI
27
28
  # Encrypt parts of a HTTP request using the given config
28
29
  #
29
30
  # @param [String] endpoint HTTP URL for the current call
30
- # @param [Object] header HTTP header
31
+ # @param [Object|nil] header HTTP header
31
32
  # @param [String,Hash] body HTTP body
32
33
  #
33
34
  # @return [Hash] Hash with two keys:
@@ -36,20 +37,21 @@ module McAPI
36
37
  #
37
38
  def encrypt(endpoint, header, body)
38
39
  body = JSON.parse(body) if body.is_a?(String)
39
- config = config?(endpoint)
40
+ config = McAPI::Utils.config?(endpoint, @config)
41
+ body_map = body
40
42
  if config
41
43
  if !@is_with_header
42
- config['toEncrypt'].each do |v|
44
+ body_map = config['toEncrypt'].map do |v|
43
45
  encrypt_with_body(v, body)
44
46
  end
45
47
  else
46
48
  enc_params = @crypto.new_encryption_params
47
- config['toEncrypt'].each do |v|
49
+ body_map = config['toEncrypt'].map do |v|
48
50
  body = encrypt_with_header(v, enc_params, header, body)
49
51
  end
50
52
  end
51
53
  end
52
- { header: header, body: body.json }
54
+ { header: header, body: config ? McAPI::Utils.compute_body(config['toEncrypt'], body_map) { body.json } : body.json }
53
55
  end
54
56
 
55
57
  #
@@ -61,35 +63,42 @@ module McAPI
61
63
  #
62
64
  def decrypt(response)
63
65
  response = JSON.parse(response)
64
- config = config?(response['request']['url'])
66
+ config = McAPI::Utils.config?(response['request']['url'], @config)
67
+ body_map = response
65
68
  if config
66
69
  if !@is_with_header
67
- config['toDecrypt'].each do |v|
70
+ body_map = config['toDecrypt'].map do |v|
68
71
  decrypt_with_body(v, response['body'])
69
72
  end
70
73
  else
71
74
  config['toDecrypt'].each do |v|
72
- elem = elem_from_path(v['obj'], response['body'])
75
+ elem = McAPI::Utils.elem_from_path(v['obj'], response['body'])
73
76
  decrypt_with_header(v, elem, response) if elem[:node][v['element']]
74
77
  end
75
78
  end
76
79
  end
80
+ response['body'] = McAPI::Utils.compute_body(config['toDecrypt'], body_map) { response['body'] } unless config.nil?
77
81
  JSON.generate(response)
78
82
  end
79
83
 
80
84
  private
81
85
 
82
86
  def encrypt_with_body(path, body)
83
- elem = elem_from_path(path['element'], body)
87
+ elem = McAPI::Utils.elem_from_path(path['element'], body)
84
88
  return unless elem && elem[:node]
89
+
85
90
  encrypted_data = @crypto.encrypt_data(data: JSON.generate(elem[:node]))
86
- McAPI::Utils.mutate_obj_prop(path['obj'], encrypted_data, body)
87
- McAPI::Utils.delete_node(path['element'], body) if path['element'] != "#{path['obj']}.#{@config['encryptedValueFieldName']}"
91
+ body = McAPI::Utils.mutate_obj_prop(path['obj'], encrypted_data, body)
92
+ unless McAPI::Utils.json_root?(path['obj']) || path['element'] == "#{path['obj']}.#{@config['encryptedValueFieldName']}"
93
+ McAPI::Utils.delete_node(path['element'], body)
94
+ end
95
+ body
88
96
  end
89
97
 
90
98
  def encrypt_with_header(path, enc_params, header, body)
91
- elem = elem_from_path(path['element'], body)
99
+ elem = McAPI::Utils.elem_from_path(path['element'], body)
92
100
  return unless elem && elem[:node]
101
+
93
102
  encrypted_data = @crypto.encrypt_data(data: JSON.generate(elem[:node]), encryption_params: enc_params)
94
103
  body = { path['obj'] => { @config['encryptedValueFieldName'] => encrypted_data[@config['encryptedValueFieldName']] } }
95
104
  set_header(header, enc_params)
@@ -97,11 +106,13 @@ module McAPI
97
106
  end
98
107
 
99
108
  def decrypt_with_body(path, body)
100
- elem = elem_from_path(path['element'], body)
109
+ elem = McAPI::Utils.elem_from_path(path['element'], body)
101
110
  return unless elem && elem[:node]
111
+
102
112
  decrypted = @crypto.decrypt_data(elem[:node][@config['encryptedValueFieldName']],
103
- elem[:node][@config['ivFieldName']],
104
- elem[:node][@config['encryptedKeyFieldName']])
113
+ elem[:node][@config['ivFieldName']],
114
+ elem[:node][@config['encryptedKeyFieldName']],
115
+ elem[:node][@config['oaepHashingAlgorithmFieldName']])
105
116
  begin
106
117
  decrypted = JSON.parse(decrypted)
107
118
  rescue JSON::ParserError
@@ -116,29 +127,8 @@ module McAPI
116
127
  response['body'].clear
117
128
  response['body'] = JSON.parse(@crypto.decrypt_data(encrypted_data,
118
129
  response['headers'][@config['ivHeaderName']][0],
119
- response['headers'][@config['encryptedKeyHeaderName']][0]))
120
- end
121
-
122
- def elem_from_path(path, obj)
123
- parent = nil
124
- paths = path.split('.')
125
- if path && !paths.empty?
126
- paths.each do |e|
127
- parent = obj
128
- obj = obj[e]
129
- end
130
- end
131
- { node: obj, parent: parent }
132
- rescue StandardError
133
- nil
134
- end
135
-
136
- def config?(endpoint)
137
- return unless endpoint
138
-
139
- endpoint = endpoint.split('?').shift
140
- conf = @config['paths'].select { |e| endpoint.match(e['path']) }
141
- conf.empty? ? nil : conf[0]
130
+ response['headers'][@config['encryptedKeyHeaderName']][0],
131
+ response['headers'][@config['oaepHashingAlgorithmHeaderName']][0]))
142
132
  end
143
133
 
144
134
  def set_header(header, params)
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'crypto/jwe-crypto'
4
+ require_relative 'utils/hash.ext'
5
+ require 'json'
6
+
7
+ module McAPI
8
+ module Encryption
9
+ #
10
+ # Performs JWE encryption on HTTP payloads.
11
+ #
12
+ class JweEncryption
13
+ #
14
+ # Create a new instance with the provided configuration
15
+ #
16
+ # @param [Hash] config Configuration object
17
+ #
18
+ def initialize(config)
19
+ @config = config
20
+ @crypto = McAPI::Encryption::JweCrypto.new(config)
21
+ end
22
+
23
+ #
24
+ # Encrypt parts of a HTTP request using the given config
25
+ #
26
+ # @param [String] endpoint HTTP URL for the current call
27
+ # @param [Object|nil] header HTTP header
28
+ # @param [String,Hash] body HTTP body
29
+ #
30
+ # @return [Hash] Hash with two keys:
31
+ # * :header header with encrypted value (if configured with header)
32
+ # * :body encrypted body
33
+ #
34
+ def encrypt(endpoint, body)
35
+ body = JSON.parse(body) if body.is_a?(String)
36
+ config = McAPI::Utils.config?(endpoint, @config)
37
+ body_map = body
38
+ if config
39
+ body_map = config['toEncrypt'].map do |v|
40
+ encrypt_with_body(v, body)
41
+ end
42
+ end
43
+ { body: config ? McAPI::Utils.compute_body(config['toEncrypt'], body_map) { body.json } : body.json }
44
+ end
45
+
46
+ #
47
+ # Decrypt part of the HTTP response using the given config
48
+ #
49
+ # @param [Object] response object as obtained from the http client
50
+ #
51
+ # @return [Object] response object with decrypted fields
52
+ #
53
+ def decrypt(response)
54
+ response = JSON.parse(response)
55
+ config = McAPI::Utils.config?(response['request']['url'], @config)
56
+ body_map = response
57
+ if config
58
+ body_map = config['toDecrypt'].map do |v|
59
+ decrypt_with_body(v, response['body'])
60
+ end
61
+ end
62
+ response['body'] = McAPI::Utils.compute_body(config['toDecrypt'], body_map) { response['body'] } unless config.nil?
63
+ JSON.generate(response)
64
+ end
65
+
66
+ private
67
+
68
+ def encrypt_with_body(path, body)
69
+ elem = McAPI::Utils.elem_from_path(path['element'], body)
70
+ return unless elem && elem[:node]
71
+
72
+ encrypted_data = @crypto.encrypt_data(data: JSON.generate(elem[:node]))
73
+ body = McAPI::Utils.mutate_obj_prop(path['obj'], encrypted_data, body)
74
+ unless McAPI::Utils.json_root?(path['obj']) || path['element'] == "#{path['obj']}.#{@config['encryptedValueFieldName']}"
75
+ McAPI::Utils.delete_node(path['element'], body)
76
+ end
77
+ body
78
+ end
79
+
80
+ def decrypt_with_body(path, body)
81
+ elem = McAPI::Utils.elem_from_path(path['element'], body)
82
+ return unless elem && elem[:node]
83
+
84
+ decrypted = @crypto.decrypt_data(encrypted_data: elem[:node][@config['encryptedValueFieldName']])
85
+ begin
86
+ decrypted = JSON.parse(decrypted)
87
+ rescue JSON::ParserError
88
+ # ignored
89
+ end
90
+
91
+ McAPI::Utils.mutate_obj_prop(path['obj'], decrypted, body, path['element'], @encryption_response_properties)
92
+ end
93
+ end
94
+ end
95
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'field_level_encryption'
4
+ require_relative 'jwe_encryption'
4
5
  require_relative 'utils/utils'
5
6
 
6
7
  module McAPI
@@ -15,7 +16,7 @@ module McAPI
15
16
  # adding encryption/decryption capabilities for the request/response payload.
16
17
  #
17
18
  # @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
+ # @param [Hash] config configuration object describing which field to enable encryption/decryption
19
20
  #
20
21
  def install_field_level_encryption(swagger_client, config)
21
22
  fle = McAPI::Encryption::FieldLevelEncryption.new(config)
@@ -27,14 +28,36 @@ module McAPI
27
28
  McAPI::Encryption::OpenAPIInterceptor.init_deserialize swagger_client
28
29
  end
29
30
 
31
+ #
32
+ # Install the JWE encryption in the OpenAPI HTTP client
33
+ # adding encryption/decryption capabilities for the request/response payload.
34
+ #
35
+ # @param [Object] swagger_client OpenAPI service client (it can be generated by the swagger code generator)
36
+ # @param [Hash] config configuration object describing which field to enable encryption/decryption
37
+ #
38
+ def install_jwe_encryption(swagger_client, config)
39
+ jwe = McAPI::Encryption::JweEncryption.new(config)
40
+ # Hooking ApiClient#call_api
41
+ hook_call_api jwe
42
+ # Hooking ApiClient#deserialize
43
+ hook_deserialize jwe
44
+ McAPI::Encryption::OpenAPIInterceptor.init_call_api swagger_client
45
+ McAPI::Encryption::OpenAPIInterceptor.init_deserialize swagger_client
46
+ end
47
+
30
48
  private
31
49
 
32
- def hook_call_api(fle)
50
+ def hook_call_api(enc)
33
51
  self.class.send :define_method, :init_call_api do |client|
34
52
  client.define_singleton_method(:call_api) do |http_method, path, opts|
35
53
  if opts && opts[:body]
36
- encrypted = fle.encrypt(path, opts[:header_params], opts[:body])
37
- opts[:body] = JSON.generate(encrypted[:body])
54
+ if enc.instance_of? McAPI::Encryption::FieldLevelEncryption
55
+ encrypted = enc.encrypt(path, opts[:header_params], opts[:body])
56
+ opts[:body] = JSON.generate(encrypted[:body])
57
+ else
58
+ encrypted = enc.encrypt(path, opts[:body])
59
+ opts[:body] = JSON.generate(encrypted[:body])
60
+ end
38
61
  end
39
62
  # noinspection RubySuperCallWithoutSuperclassInspection
40
63
  super(http_method, path, opts)
@@ -42,15 +65,20 @@ module McAPI
42
65
  end
43
66
  end
44
67
 
45
- def hook_deserialize(fle)
68
+ def hook_deserialize(enc)
46
69
  self.class.send :define_method, :init_deserialize do |client|
47
70
  client.define_singleton_method(:deserialize) do |response, return_type|
48
- if response && response.body
71
+ if response&.body
49
72
  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))
73
+ if enc.instance_of? McAPI::Encryption::FieldLevelEncryption
74
+ to_decrypt = { headers: McAPI::Utils.parse_header(response.options[:response_headers]),
75
+ request: { url: endpoint },
76
+ body: JSON.parse(response.body) }
77
+ else
78
+ to_decrypt = { request: { url: endpoint },
79
+ body: JSON.parse(response.body) }
80
+ end
81
+ decrypted = enc.decrypt(JSON.generate(to_decrypt, symbolize_names: false))
54
82
  body = JSON.generate(JSON.parse(decrypted)['body'])
55
83
  response.options[:response_body] = JSON.generate(JSON.parse(body))
56
84
  end
@@ -51,7 +51,7 @@ module OpenSSL
51
51
  em = str.bytes
52
52
  raise OpenSSL::PKey::RSAError if em.size < 2 * mdlen + 2
53
53
 
54
- # Keep constant calculation even if the text is invaid in order to avoid attacks.
54
+ # Keep constant calculation even if the text is invalid in order to avoid attacks.
55
55
  good = secure_byte_is_zero(em[0])
56
56
  masked_seed = em[1...1 + mdlen].pack('C*')
57
57
  masked_db = em[1 + mdlen...em.size].pack('C*')
@@ -66,22 +66,27 @@ module McAPI
66
66
  def self.mutate_obj_prop(path, value, obj, src_path = nil, properties = [])
67
67
  tmp = obj
68
68
  prev = nil
69
- return unless path
69
+ return obj unless path
70
70
 
71
71
  delete_node(src_path, obj, properties) if src_path
72
72
  paths = path.split('.')
73
- paths.each do |e|
74
- tmp[e] = {} unless tmp[e]
75
- prev = tmp
76
- tmp = tmp[e]
73
+ unless path == '$'
74
+ paths.each do |e|
75
+ tmp[e] = {} unless tmp[e]
76
+ prev = tmp
77
+ tmp = tmp[e]
78
+ end
77
79
  end
78
80
  elem = path.split('.').pop
79
- if value.is_a?(Hash) && !value.is_a?(Array)
81
+ if elem == '$'
82
+ obj = value # replace root
83
+ elsif value.is_a?(Hash) && !value.is_a?(Array)
80
84
  prev[elem] = {} unless prev[elem].is_a?(Hash)
81
85
  override_props(prev[elem], value)
82
86
  else
83
87
  prev[elem] = value
84
88
  end
89
+ obj
85
90
  end
86
91
 
87
92
  def self.override_props(target, obj)
@@ -105,6 +110,7 @@ module McAPI
105
110
  obj = obj[e]
106
111
  prev.delete(to_delete) if obj && index == paths.size - 1
107
112
  end
113
+ obj.keys.each { |e| obj.delete(e) } if paths.length == 1 && paths[0] == '$'
108
114
  properties.each { |e| obj.delete(e) } if paths.empty?
109
115
  end
110
116
 
@@ -140,5 +146,45 @@ module McAPI
140
146
  end
141
147
  header
142
148
  end
149
+
150
+ #
151
+ # Get an element from the JSON path
152
+ #
153
+ def self.elem_from_path(path, obj)
154
+ parent = nil
155
+ paths = path.split('.')
156
+ if path && !paths.empty?
157
+ paths.each do |e|
158
+ parent = obj
159
+ obj = json_root?(e) ? obj : obj[e]
160
+ end
161
+ end
162
+ { node: obj, parent: parent }
163
+ rescue StandardError
164
+ nil
165
+ end
166
+
167
+ #
168
+ # Check whether the encryption/decryption path refers to the root element
169
+ #
170
+ def self.json_root?(elem)
171
+ elem == '$'
172
+ end
173
+
174
+ def self.config?(endpoint, config)
175
+ return unless endpoint
176
+
177
+ endpoint = endpoint.split('?').shift
178
+ conf = config['paths'].select { |e| endpoint.match(e['path']) }
179
+ conf.empty? ? nil : conf[0]
180
+ end
181
+
182
+ def self.compute_body(config_param, body_map)
183
+ encryption_param?(config_param, body_map) ? body_map[0] : yield
184
+ end
185
+
186
+ def self.encryption_param?(enc_param, body_map)
187
+ enc_param.length == 1 && body_map.length == 1
188
+ end
143
189
  end
144
190
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mastercard-client-encryption
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mastercard
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-08-13 00:00:00.000000000 Z
11
+ date: 2022-07-21 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: hamster
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: bundler
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -42,16 +56,16 @@ dependencies:
42
56
  name: rake
43
57
  requirement: !ruby/object:Gem::Requirement
44
58
  requirements:
45
- - - "~>"
59
+ - - ">="
46
60
  - !ruby/object:Gem::Version
47
- version: '10.0'
61
+ version: 12.3.3
48
62
  type: :development
49
63
  prerelease: false
50
64
  version_requirements: !ruby/object:Gem::Requirement
51
65
  requirements:
52
- - - "~>"
66
+ - - ">="
53
67
  - !ruby/object:Gem::Version
54
- version: '10.0'
68
+ version: 12.3.3
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: simplecov
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -73,7 +87,9 @@ extensions: []
73
87
  extra_rdoc_files: []
74
88
  files:
75
89
  - lib/mcapi/encryption/crypto/crypto.rb
90
+ - lib/mcapi/encryption/crypto/jwe-crypto.rb
76
91
  - lib/mcapi/encryption/field_level_encryption.rb
92
+ - lib/mcapi/encryption/jwe_encryption.rb
77
93
  - lib/mcapi/encryption/openapi_interceptor.rb
78
94
  - lib/mcapi/encryption/utils/hash.ext.rb
79
95
  - lib/mcapi/encryption/utils/openssl_rsa_oaep.rb