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

Commit 9b9f382

Browse files
* Adding JWE support
1 parent 1673c4f commit 9b9f382

18 files changed

+628
-59
lines changed
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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+
def initialize(config)
17+
@encoding = config['dataEncoding']
18+
@cert = OpenSSL::X509::Certificate.new(IO.binread(config['encryptionCertificate']))
19+
if config['privateKey']
20+
@private_key = OpenSSL::PKey.read(IO.binread(config['privateKey']))
21+
elsif config['keyStore']
22+
@private_key = OpenSSL::PKCS12.new(IO.binread(config['keyStore']), config['keyStorePassword']).key
23+
end
24+
@encrypted_value_field_name = config['encryptedValueFieldName'] || 'encryptedData'
25+
@public_key_fingerprint = compute_public_fingerprint
26+
end
27+
28+
def encrypt_data(data:)
29+
cek = SecureRandom.random_bytes(32)
30+
iv = SecureRandom.random_bytes(12)
31+
32+
md = OpenSSL::Digest::SHA256
33+
encrypted_key = @cert.public_key.public_encrypt_oaep(cek, '', md, md)
34+
35+
header = generate_header('RSA-OAEP-256', 'A256GCM')
36+
json_hdr = header.to_json
37+
auth_data = jwe_encode(json_hdr)
38+
39+
cipher = OpenSSL::Cipher.new('aes-256-gcm')
40+
cipher.encrypt
41+
cipher.key = cek
42+
cipher.iv = iv
43+
cipher.padding = 0
44+
cipher.auth_data = auth_data
45+
cipher_text = cipher.update(data) + cipher.final
46+
47+
payload = generate_serialization(json_hdr, encrypted_key, cipher_text, iv, cipher.auth_tag)
48+
{
49+
@encrypted_value_field_name => payload
50+
}
51+
end
52+
53+
def decrypt_data(encrypted_data:)
54+
parts = encrypted_data.split('.')
55+
encrypted_header, encrypted_key, initialization_vector, cipher_text, authentication_tag = parts
56+
57+
jwe_header = jwe_decode(encrypted_header)
58+
encrypted_key = jwe_decode(encrypted_key)
59+
iv = jwe_decode(initialization_vector)
60+
cipher_text = jwe_decode(cipher_text)
61+
cipher_tag = jwe_decode(authentication_tag)
62+
63+
md = OpenSSL::Digest::SHA256
64+
cek = @private_key.private_decrypt_oaep(encrypted_key, '', md, md)
65+
66+
enc_method = JSON.parse(jwe_header)['enc']
67+
68+
if enc_method == "A256GCM"
69+
enc_string = "aes-256-gcm"
70+
elsif enc_method == "A128CBC-HS256"
71+
enc_string = "aes-128-cbc"
72+
else
73+
raise Exception, "Encryption method '#{enc_method}' not supported."
74+
end
75+
76+
cipher = OpenSSL::Cipher.new(enc_string)
77+
cipher.decrypt
78+
cipher.key = cek
79+
cipher.iv = iv
80+
cipher.padding = 0
81+
cipher.auth_data = encrypted_header
82+
cipher.auth_tag = cipher_tag
83+
84+
plain_text = cipher.update(cipher_text) + cipher.final
85+
plain_text
86+
end
87+
88+
private
89+
90+
def compute_public_fingerprint
91+
OpenSSL::Digest::SHA256.new(@cert.public_key.to_der).to_s
92+
end
93+
94+
def generate_header(alg, enc)
95+
header = { alg: alg, enc: enc, kid: @public_key_fingerprint, cty: 'application/json' }
96+
header
97+
end
98+
99+
def jwe_encode(payload)
100+
::Base64.urlsafe_encode64(payload).delete('=')
101+
end
102+
103+
def jwe_decode(payload)
104+
padlen = 4 - (payload.length % 4)
105+
if padlen < 4
106+
pad = '=' * padlen
107+
payload += pad
108+
end
109+
::Base64.urlsafe_decode64(payload)
110+
end
111+
112+
def generate_serialization(hdr, cek, content, iv, tag)
113+
[hdr, cek, iv, content, tag].map { |piece| jwe_encode(piece) }.join '.'
114+
end
115+
end
116+
end
117+
end

lib/mcapi/encryption/field_level_encryption.rb

Lines changed: 10 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
require_relative 'crypto/crypto'
44
require_relative 'utils/hash.ext'
5+
require_relative 'utils/utils'
56
require 'json'
67

78
module McAPI
@@ -36,7 +37,7 @@ def initialize(config)
3637
#
3738
def encrypt(endpoint, header, body)
3839
body = JSON.parse(body) if body.is_a?(String)
39-
config = config?(endpoint)
40+
config = McAPI::Utils.config?(endpoint, @config)
4041
body_map = body
4142
if config
4243
if !@is_with_header
@@ -50,7 +51,7 @@ def encrypt(endpoint, header, body)
5051
end
5152
end
5253
end
53-
{ header: header, body: config ? compute_body(config['toEncrypt'], body_map) { body.json } : body.json }
54+
{ header: header, body: config ? McAPI::Utils.compute_body(config['toEncrypt'], body_map) { body.json } : body.json }
5455
end
5556

5657
#
@@ -62,7 +63,7 @@ def encrypt(endpoint, header, body)
6263
#
6364
def decrypt(response)
6465
response = JSON.parse(response)
65-
config = config?(response['request']['url'])
66+
config = McAPI::Utils.config?(response['request']['url'], @config)
6667
body_map = response
6768
if config
6869
if !@is_with_header
@@ -71,31 +72,31 @@ def decrypt(response)
7172
end
7273
else
7374
config['toDecrypt'].each do |v|
74-
elem = elem_from_path(v['obj'], response['body'])
75+
elem = McAPI::Utils.elem_from_path(v['obj'], response['body'])
7576
decrypt_with_header(v, elem, response) if elem[:node][v['element']]
7677
end
7778
end
7879
end
79-
response['body'] = compute_body(config['toDecrypt'], body_map) { response['body'] } unless config.nil?
80+
response['body'] = McAPI::Utils.compute_body(config['toDecrypt'], body_map) { response['body'] } unless config.nil?
8081
JSON.generate(response)
8182
end
8283

8384
private
8485

8586
def encrypt_with_body(path, body)
86-
elem = elem_from_path(path['element'], body)
87+
elem = McAPI::Utils.elem_from_path(path['element'], body)
8788
return unless elem && elem[:node]
8889

8990
encrypted_data = @crypto.encrypt_data(data: JSON.generate(elem[:node]))
9091
body = McAPI::Utils.mutate_obj_prop(path['obj'], encrypted_data, body)
91-
unless json_root?(path['obj']) || path['element'] == "#{path['obj']}.#{@config['encryptedValueFieldName']}"
92+
unless McAPI::Utils.json_root?(path['obj']) || path['element'] == "#{path['obj']}.#{@config['encryptedValueFieldName']}"
9293
McAPI::Utils.delete_node(path['element'], body)
9394
end
9495
body
9596
end
9697

9798
def encrypt_with_header(path, enc_params, header, body)
98-
elem = elem_from_path(path['element'], body)
99+
elem = McAPI::Utils.elem_from_path(path['element'], body)
99100
return unless elem && elem[:node]
100101

101102
encrypted_data = @crypto.encrypt_data(data: JSON.generate(elem[:node]), encryption_params: enc_params)
@@ -105,7 +106,7 @@ def encrypt_with_header(path, enc_params, header, body)
105106
end
106107

107108
def decrypt_with_body(path, body)
108-
elem = elem_from_path(path['element'], body)
109+
elem = McAPI::Utils.elem_from_path(path['element'], body)
109110
return unless elem && elem[:node]
110111

111112
decrypted = @crypto.decrypt_data(elem[:node][@config['encryptedValueFieldName']],
@@ -130,46 +131,12 @@ def decrypt_with_header(path, elem, response)
130131
response['headers'][@config['oaepHashingAlgorithmHeaderName']][0]))
131132
end
132133

133-
def elem_from_path(path, obj)
134-
parent = nil
135-
paths = path.split('.')
136-
if path && !paths.empty?
137-
paths.each do |e|
138-
parent = obj
139-
obj = json_root?(e) ? obj : obj[e]
140-
end
141-
end
142-
{ node: obj, parent: parent }
143-
rescue StandardError
144-
nil
145-
end
146-
147-
def config?(endpoint)
148-
return unless endpoint
149-
150-
endpoint = endpoint.split('?').shift
151-
conf = @config['paths'].select { |e| endpoint.match(e['path']) }
152-
conf.empty? ? nil : conf[0]
153-
end
154-
155134
def set_header(header, params)
156135
header[@config['encryptedKeyHeaderName']] = params[:encoded][:encryptedKey]
157136
header[@config['ivHeaderName']] = params[:encoded][:iv]
158137
header[@config['oaepHashingAlgorithmHeaderName']] = params[:oaepHashingAlgorithm].sub('-', '')
159138
header[@config['publicKeyFingerprintHeaderName']] = params[:publicKeyFingerprint]
160139
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
173140
end
174141
end
175142
end
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

0 commit comments

Comments
 (0)