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 +4 -4
- data/lib/mcapi/encryption/crypto/crypto.rb +11 -10
- 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 +38 -10
- data/lib/mcapi/encryption/utils/openssl_rsa_oaep.rb +1 -1
- data/lib/mcapi/encryption/utils/utils.rb +52 -6
- metadata +22 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 498fe9a62ff880357316d0dd97d78d27dae85bca253702d905e2769087579ba2
|
4
|
+
data.tar.gz: 22a35a9afebde5f8f746067154c339ced42edd4b0d4ec0d6fa920963f1de5abd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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(
|
23
|
+
@cert = OpenSSL::X509::Certificate.new(IO.binread(config['encryptionCertificate']))
|
24
24
|
if config['privateKey']
|
25
|
-
@private_key = OpenSSL::PKey.read(
|
25
|
+
@private_key = OpenSSL::PKey.read(IO.binread(config['privateKey']))
|
26
26
|
elsif config['keyStore']
|
27
|
-
@private_key = OpenSSL::PKCS12.new(
|
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(
|
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,20 @@ 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
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
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
|
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.0
|
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:
|
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:
|
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:
|
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
|