mastercard-client-encryption 1.0.3 → 1.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/mcapi/encryption/crypto/crypto.rb +8 -7
- data/lib/mcapi/encryption/crypto/jwe-crypto.rb +175 -0
- data/lib/mcapi/encryption/field_level_encryption.rb +29 -39
- data/lib/mcapi/encryption/jwe_encryption.rb +95 -0
- data/lib/mcapi/encryption/openapi_interceptor.rb +31 -7
- data/lib/mcapi/encryption/utils/openssl_rsa_oaep.rb +1 -1
- data/lib/mcapi/encryption/utils/utils.rb +52 -6
- metadata +18 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bbccd35b77756bd0991d1baf67d1870fdd50f51a6c2e134720bc7a5ce5323212
|
4
|
+
data.tar.gz: 3b39bd9f88a24178460035dae148605fac7fbb6b8612828d9ee80e1dd52ad7a2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cb72b048455038d96346ba38377fdd9d4c8cee1952961800ecef92598a5355f0bf2de5c2ac8b6eb086a93b70b1c43e818a7a5239b0511af626b45df346f78878
|
7
|
+
data.tar.gz: 9a4b559336e6525cc06826d3bfc45cec2b3dd7c72e1a3e0803eccaf9eaa4cef68512b7e630e2a01ca98f829dc14b52be8c00d28a5299860aa2dc9f6e1309e25b
|
@@ -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(
|
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 [
|
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 [
|
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'].
|
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'].
|
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'].
|
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.
|
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
|
-
|
104
|
-
|
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
|
-
|
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 [
|
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(
|
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
|
-
|
37
|
-
|
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,16 @@ module McAPI
|
|
42
65
|
end
|
43
66
|
end
|
44
67
|
|
45
|
-
def hook_deserialize(
|
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
|
71
|
+
if response&.body
|
49
72
|
endpoint = response.request.base_url.sub client.config.base_url, ''
|
50
73
|
to_decrypt = { headers: McAPI::Utils.parse_header(response.options[:response_headers]),
|
51
74
|
request: { url: endpoint },
|
52
75
|
body: JSON.parse(response.body) }
|
53
|
-
|
76
|
+
|
77
|
+
decrypted = enc.decrypt(JSON.generate(to_decrypt, symbolize_names: false))
|
54
78
|
body = JSON.generate(JSON.parse(decrypted)['body'])
|
55
79
|
response.options[:response_body] = JSON.generate(JSON.parse(body))
|
56
80
|
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
|
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
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
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
|
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.
|
4
|
+
version: 1.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mastercard
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
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
|
@@ -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
|