Skip to content
This repository was archived by the owner on Feb 28, 2024. It is now read-only.

Commit 8fc87cc

Browse files
authored
Ability to encrypt/decrypt array bodies (#5)
* Added ability to encrypt/decrypt array bodies + clean-up * Fixed sonar scan not running on pull requests
1 parent 41b88ed commit 8fc87cc

File tree

10 files changed

+161
-37
lines changed

10 files changed

+161
-37
lines changed

.github/workflows/sonar.yml

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ name: Sonar
99
schedule:
1010
- cron: 0 16 * * *
1111
jobs:
12-
test:
12+
sonarcloud:
1313
runs-on: ubuntu-latest
1414
steps:
1515
- uses: actions/checkout@v2
@@ -40,11 +40,6 @@ jobs:
4040
SONAR_TOKEN: '${{ secrets.SONAR_TOKEN }}'
4141
run: |
4242
sonar-scanner-3.3.0.1492/bin/sonar-scanner \
43-
-Dsonar.projectName=client-encryption-ruby \
44-
-Dsonar.projectKey=Mastercard_client-encryption-ruby \
45-
-Dsonar.organization=mastercard \
4643
-Dsonar.sources=./lib \
4744
-Dsonar.tests=./test \
48-
-Dsonar.ruby.coverage.reportPaths=coverage/.resultset.json \
49-
-Dsonar.host.url=https://sonarcloud.io \
50-
-Dsonar.login=$SONAR_TOKEN
45+
-Dsonar.ruby.coverage.reportPaths=coverage/.resultset.json

lib/mcapi/encryption/crypto/crypto.rb

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ def initialize(config)
3737
#
3838
# Generate encryption parameters.
3939
#
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
4242
#
4343
# @return [Hash] hash with the generated encryption parameters
4444
#
@@ -70,8 +70,8 @@ def new_encryption_params(iv = nil, secret_key = nil)
7070
# If +iv+, +secret_key+, +encryption_params+ and +encoding+ are not provided, randoms will be generated.
7171
#
7272
# @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
7575
# @param [String] encoding encoding to use for the encrypted bytes (hex or base64)
7676
#
7777
# @return [String] encrypted data
@@ -103,11 +103,12 @@ def encrypt_data(data:, iv: nil, secret_key: nil, encryption_params: nil, encodi
103103
# @param [String] iv Initialization vector to use to create the Decipher
104104
# @param [String] encrypted_key Encrypted key to use to decrypt the data
105105
# (the key is the decrypted using the provided PrivateKey)
106+
# @param [String] oaep_hashing_alg OAEP Algorithm to use
106107
#
107108
# @return [String] Decrypted JSON object
108109
#
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)
111112
decrypted_key = @private_key.private_decrypt_oaep(Utils.decode(encrypted_key, @encoding), '', md, md)
112113
aes = OpenSSL::Cipher::AES.new(decrypted_key.size * 8, :CBC)
113114
aes.decrypt
@@ -121,7 +122,7 @@ def decrypt_data(encrypted_data, iv, encrypted_key)
121122
#
122123
# Compute the fingerprint for the provided public key
123124
#
124-
# @param [String] type: +certificate+ or +publickey+
125+
# @param [Hash] type: +certificate+ or +publickey+
125126
#
126127
# @return [String] the computed fingerprint encoded using the configured encoding
127128
#

lib/mcapi/encryption/field_level_encryption.rb

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class FieldLevelEncryption
1313
#
1414
# Create a new instance with the provided configuration
1515
#
16-
# @param [Object] config Configuration object
16+
# @param [Hash] config Configuration object
1717
#
1818
def initialize(config)
1919
@config = config
@@ -27,7 +27,7 @@ def initialize(config)
2727
# Encrypt parts of a HTTP request using the given config
2828
#
2929
# @param [String] endpoint HTTP URL for the current call
30-
# @param [Object] header HTTP header
30+
# @param [Object|nil] header HTTP header
3131
# @param [String,Hash] body HTTP body
3232
#
3333
# @return [Hash] Hash with two keys:
@@ -37,19 +37,20 @@ def initialize(config)
3737
def encrypt(endpoint, header, body)
3838
body = JSON.parse(body) if body.is_a?(String)
3939
config = config?(endpoint)
40+
body_map = body
4041
if config
4142
if !@is_with_header
42-
config['toEncrypt'].each do |v|
43+
body_map = config['toEncrypt'].map do |v|
4344
encrypt_with_body(v, body)
4445
end
4546
else
4647
enc_params = @crypto.new_encryption_params
47-
config['toEncrypt'].each do |v|
48+
body_map = config['toEncrypt'].map do |v|
4849
body = encrypt_with_header(v, enc_params, header, body)
4950
end
5051
end
5152
end
52-
{ header: header, body: body.json }
53+
{ header: header, body: config ? compute_body(config['toEncrypt'], body_map) { body.json } : body.json }
5354
end
5455

5556
#
@@ -62,9 +63,10 @@ def encrypt(endpoint, header, body)
6263
def decrypt(response)
6364
response = JSON.parse(response)
6465
config = config?(response['request']['url'])
66+
body_map = response
6567
if config
6668
if !@is_with_header
67-
config['toDecrypt'].each do |v|
69+
body_map = config['toDecrypt'].map do |v|
6870
decrypt_with_body(v, response['body'])
6971
end
7072
else
@@ -74,6 +76,7 @@ def decrypt(response)
7476
end
7577
end
7678
end
79+
response['body'] = compute_body(config['toDecrypt'], body_map) { response['body'] } unless config.nil?
7780
JSON.generate(response)
7881
end
7982

@@ -82,14 +85,19 @@ def decrypt(response)
8285
def encrypt_with_body(path, body)
8386
elem = elem_from_path(path['element'], body)
8487
return unless elem && elem[:node]
88+
8589
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']}"
90+
body = McAPI::Utils.mutate_obj_prop(path['obj'], encrypted_data, body)
91+
unless json_root?(path['obj']) || path['element'] == "#{path['obj']}.#{@config['encryptedValueFieldName']}"
92+
McAPI::Utils.delete_node(path['element'], body)
93+
end
94+
body
8895
end
8996

9097
def encrypt_with_header(path, enc_params, header, body)
9198
elem = elem_from_path(path['element'], body)
9299
return unless elem && elem[:node]
100+
93101
encrypted_data = @crypto.encrypt_data(data: JSON.generate(elem[:node]), encryption_params: enc_params)
94102
body = { path['obj'] => { @config['encryptedValueFieldName'] => encrypted_data[@config['encryptedValueFieldName']] } }
95103
set_header(header, enc_params)
@@ -99,9 +107,11 @@ def encrypt_with_header(path, enc_params, header, body)
99107
def decrypt_with_body(path, body)
100108
elem = elem_from_path(path['element'], body)
101109
return unless elem && elem[:node]
110+
102111
decrypted = @crypto.decrypt_data(elem[:node][@config['encryptedValueFieldName']],
103-
elem[:node][@config['ivFieldName']],
104-
elem[:node][@config['encryptedKeyFieldName']])
112+
elem[:node][@config['ivFieldName']],
113+
elem[:node][@config['encryptedKeyFieldName']],
114+
elem[:node][@config['oaepHashingAlgorithmFieldName']])
105115
begin
106116
decrypted = JSON.parse(decrypted)
107117
rescue JSON::ParserError
@@ -116,7 +126,8 @@ def decrypt_with_header(path, elem, response)
116126
response['body'].clear
117127
response['body'] = JSON.parse(@crypto.decrypt_data(encrypted_data,
118128
response['headers'][@config['ivHeaderName']][0],
119-
response['headers'][@config['encryptedKeyHeaderName']][0]))
129+
response['headers'][@config['encryptedKeyHeaderName']][0],
130+
response['headers'][@config['oaepHashingAlgorithmHeaderName']][0]))
120131
end
121132

122133
def elem_from_path(path, obj)
@@ -125,7 +136,7 @@ def elem_from_path(path, obj)
125136
if path && !paths.empty?
126137
paths.each do |e|
127138
parent = obj
128-
obj = obj[e]
139+
obj = json_root?(e) ? obj : obj[e]
129140
end
130141
end
131142
{ node: obj, parent: parent }
@@ -147,6 +158,18 @@ def set_header(header, params)
147158
header[@config['oaepHashingAlgorithmHeaderName']] = params[:oaepHashingAlgorithm].sub('-', '')
148159
header[@config['publicKeyFingerprintHeaderName']] = params[:publicKeyFingerprint]
149160
end
161+
162+
def json_root?(elem)
163+
elem == '$'
164+
end
165+
166+
def compute_body(config_param, body_map)
167+
encryption_param?(config_param, body_map) ? body_map[0] : yield
168+
end
169+
170+
def encryption_param?(enc_param, body_map)
171+
enc_param.length == 1 && body_map.length == 1
172+
end
150173
end
151174
end
152175
end

lib/mcapi/encryption/openapi_interceptor.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class << self
1515
# adding encryption/decryption capabilities for the request/response payload.
1616
#
1717
# @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
18+
# @param [Hash] config configuration object describing which field to enable encryption/decryption
1919
#
2020
def install_field_level_encryption(swagger_client, config)
2121
fle = McAPI::Encryption::FieldLevelEncryption.new(config)
@@ -45,7 +45,7 @@ def hook_call_api(fle)
4545
def hook_deserialize(fle)
4646
self.class.send :define_method, :init_deserialize do |client|
4747
client.define_singleton_method(:deserialize) do |response, return_type|
48-
if response && response.body
48+
if response&.body
4949
endpoint = response.request.base_url.sub client.config.base_url, ''
5050
to_decrypt = { headers: McAPI::Utils.parse_header(response.options[:response_headers]),
5151
request: { url: endpoint },

lib/mcapi/encryption/utils/openssl_rsa_oaep.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def check_oaep_mgf1(str, label = '', md = nil, mgf1md = nil)
5151
em = str.bytes
5252
raise OpenSSL::PKey::RSAError if em.size < 2 * mdlen + 2
5353

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.
5555
good = secure_byte_is_zero(em[0])
5656
masked_seed = em[1...1 + mdlen].pack('C*')
5757
masked_db = em[1 + mdlen...em.size].pack('C*')

lib/mcapi/encryption/utils/utils.rb

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,22 +66,27 @@ def self.contains(config, props)
6666
def self.mutate_obj_prop(path, value, obj, src_path = nil, properties = [])
6767
tmp = obj
6868
prev = nil
69-
return unless path
69+
return obj unless path
7070

7171
delete_node(src_path, obj, properties) if src_path
7272
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
7779
end
7880
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)
8084
prev[elem] = {} unless prev[elem].is_a?(Hash)
8185
override_props(prev[elem], value)
8286
else
8387
prev[elem] = value
8488
end
89+
obj
8590
end
8691

8792
def self.override_props(target, obj)
@@ -105,6 +110,7 @@ def self.delete_node(path, obj, properties = [])
105110
obj = obj[e]
106111
prev.delete(to_delete) if obj && index == paths.size - 1
107112
end
113+
obj.keys.each { |e| obj.delete(e) } if paths.length == 1 && paths[0] == '$'
108114
properties.each { |e| obj.delete(e) } if paths.empty?
109115
end
110116

sonar-project.properties

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
sonar.projectKey=Mastercard_client-encryption-ruby
2+
sonar.organization=mastercard
3+
sonar.projectName=client-encryption-ruby
4+
sonar.host.url=https://sonarcloud.io

test/mock/config.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,36 @@
2929
"obj": "foo"
3030
}
3131
]
32+
},
33+
{
34+
"path": "/array-resp$",
35+
"toEncrypt": [
36+
{
37+
"element": "$",
38+
"obj": "$"
39+
}
40+
],
41+
"toDecrypt": [
42+
{
43+
"element": "$",
44+
"obj": "$"
45+
}
46+
]
47+
},
48+
{
49+
"path": "/array-resp2",
50+
"toEncrypt": [
51+
{
52+
"element": "$",
53+
"obj": "$"
54+
}
55+
],
56+
"toDecrypt": [
57+
{
58+
"element": "$",
59+
"obj": "path.to.foo"
60+
}
61+
]
3262
}
3363
],
3464
"oaepPaddingDigestAlgorithm": "SHA-512",

test/test_crypto_cryptography.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ def test_encrypt_valid_obj_key_and_iv
2020
def test_decrypt_valid_obj
2121
resp = @crypto.decrypt_data('3590b63d1520a57bd4cd1414a7a75f47d65f99e1427d6cfe744d72ee60f2b232',
2222
'6f38f3ecd8b92c2fd2537a7235deb9a8',
23-
'e283a661efa235fbc5e7243b7b78914a7f33574eb66cc1854829f7debfce4163f3ce86ad2c3ed2c8fe97b2258ab8a158281147698b7fddf5e82544b0b637353d2c204798f014112a5e278db0b29ad852b1417dc761593fad3f0a1771797771796dc1e8ae916adaf3f4486aa79af9d4028bc8d17399d50c80667ea73a8a5d1341a9160f9422aaeb0b4667f345ea637ac993e80a452cb8341468483b7443f764967264aaebb2cad4513e4922d076a094afebcf1c71b53ba3cfedb736fa2ca5de5c1e2aa88b781d30c27debd28c2f5d83e89107d5214e3bb3fe186412d78cefe951e384f236e55cd3a67fb13c0d6950f097453f76e7679143bd4e62d986ce9dc770')
23+
'e283a661efa235fbc5e7243b7b78914a7f33574eb66cc1854829f7debfce4163f3ce86ad2c3ed2c8fe97b2258ab8a158281147698b7fddf5e82544b0b637353d2c204798f014112a5e278db0b29ad852b1417dc761593fad3f0a1771797771796dc1e8ae916adaf3f4486aa79af9d4028bc8d17399d50c80667ea73a8a5d1341a9160f9422aaeb0b4667f345ea637ac993e80a452cb8341468483b7443f764967264aaebb2cad4513e4922d076a094afebcf1c71b53ba3cfedb736fa2ca5de5c1e2aa88b781d30c27debd28c2f5d83e89107d5214e3bb3fe186412d78cefe951e384f236e55cd3a67fb13c0d6950f097453f76e7679143bd4e62d986ce9dc770',
24+
'SHA-512')
2425
assert_equal resp, '{"text":"message"}'
2526
end
2627

@@ -37,7 +38,8 @@ def test_decrypt3_aes
3738
# iv
3839
'fb2057968fa06067b6ab4c732c32cbcd',
3940
# key
40-
'7686c2472f8d53175074dd2830b4f875753343e59eec16a131f26e9e8026c3052993d8c9ad6eba04048f6a54b64160a13da28333816dfc178db2ed30068519d211c84fd7edc79838b58e97bb688b46215614308760e49d2fec95bfdf0570ce9fc5cdf814dca0dfface3d67b24b743d6003a072a882c1662ee24a9adf8b4d5825b5be74e6b73f9d08a8a2099a3fb875240ada002397c47be8a71c74e864bf8b1654365ddd2efe7b2ee44a75e08979993bfc1727cb8304607e295cab2e2dd8a8776e9678e8b9653b7e831d7b50a08d5ed1ac8c15f2933bcefef8d5b160d3a296bbdeac9d355879c0f8fc97860e17537465534095581374e9f29b1c10c7e860a638'
41+
'7686c2472f8d53175074dd2830b4f875753343e59eec16a131f26e9e8026c3052993d8c9ad6eba04048f6a54b64160a13da28333816dfc178db2ed30068519d211c84fd7edc79838b58e97bb688b46215614308760e49d2fec95bfdf0570ce9fc5cdf814dca0dfface3d67b24b743d6003a072a882c1662ee24a9adf8b4d5825b5be74e6b73f9d08a8a2099a3fb875240ada002397c47be8a71c74e864bf8b1654365ddd2efe7b2ee44a75e08979993bfc1727cb8304607e295cab2e2dd8a8776e9678e8b9653b7e831d7b50a08d5ed1ac8c15f2933bcefef8d5b160d3a296bbdeac9d355879c0f8fc97860e17537465534095581374e9f29b1c10c7e860a638',
42+
'SHA-256'
4143
)
4244
resp = JSON.parse(resp)
4345
assert !resp['mapping'].nil?

0 commit comments

Comments
 (0)