-
-
Notifications
You must be signed in to change notification settings - Fork 590
Expand file tree
/
Copy pathdecryptor.rb
More file actions
235 lines (204 loc) · 10.2 KB
/
decryptor.rb
File metadata and controls
235 lines (204 loc) · 10.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# frozen_string_literal: true
module RubySaml
module XML
# Module for handling document decryption
module Decryptor
extend self
# Generates decrypted document with assertions decrypted
# @param document [Nokogiri::XML::Document] The encrypted SAML document
# @param decryption_keys [Array] Array of private keys for decryption
# @return [Nokogiri::XML::Document] The SAML document with assertions decrypted
def decrypt_document(document, decryption_keys)
# Copy the document to avoid modifying the original one
begin
document = RubySaml::XML.safe_load_xml(document.to_s, check_malformed_doc: true)
rescue StandardError => e
raise ValidationError.new("XML load failed: #{e.message}") if e.message != 'Empty document'
end
validate_decryption_keys!(decryption_keys)
response_node = document.at_xpath(
'/p:Response',
{ 'p' => RubySaml::XML::NS_PROTOCOL }
)
encrypted_assertion_node = document.at_xpath(
'/p:Response/EncryptedAssertion | /p:Response/a:EncryptedAssertion',
{ 'p' => RubySaml::XML::NS_PROTOCOL, 'a' => RubySaml::XML::NS_ASSERTION }
)
if encrypted_assertion_node && response_node
response_node.add_child(decrypt_assertion(encrypted_assertion_node, decryption_keys))
encrypted_assertion_node.remove
end
document
end
# Decrypts an EncryptedAssertion element
# @param encrypted_assertion_node [Nokogiri::XML::Element] The EncryptedAssertion element
# @param decryption_keys [Array] Array of private keys for decryption
# @return [Nokogiri::XML::Document] The decrypted EncryptedAssertion element
def decrypt_assertion(encrypted_assertion_node, decryption_keys)
decrypt_node(encrypted_assertion_node, %r{(.*</(\w+:)?Assertion>)}m, decryption_keys)
end
# Decrypts an EncryptedID element
# @param encrypted_id_node [Nokogiri::XML::Element] The EncryptedID element
# @param decryption_keys [Array] Array of private keys for decryption
# @return [Nokogiri::XML::Document] The decrypted EncryptedID element
def decrypt_nameid(encrypted_id_node, decryption_keys)
decrypt_node(encrypted_id_node, %r{(.*</(\w+:)?NameID>)}m, decryption_keys)
end
# Decrypts an EncryptedAttribute element
# @param encrypted_attribute_node [Nokogiri::XML::Element] The EncryptedAttribute element
# @param decryption_keys [Array] Array of private keys for decryption
# @return [Nokogiri::XML::Document] The decrypted EncryptedAttribute element
def decrypt_attribute(encrypted_attribute_node, decryption_keys)
decrypt_node(encrypted_attribute_node, %r{(.*</(\w+:)?Attribute>)}m, decryption_keys)
end
# Decrypt an element
# @param encrypted_node [Nokogiri::XML::Element] The encrypted element
# @param regexp [Regexp] The regular expression to extract the decrypted data
# @param decryption_keys [Array] Array of private keys for decryption
# @return [Nokogiri::XML::Document] The decrypted element
def decrypt_node(encrypted_node, regexp, decryption_keys)
validate_decryption_keys!(decryption_keys)
node_header = if encrypted_node.name == 'EncryptedAttribute'
%(<node xmlns:saml="#{RubySaml::XML::NS_ASSERTION}" xmlns:xsi="#{RubySaml::XML::XSI}">)
else
%(<node xmlns:saml="#{RubySaml::XML::NS_ASSERTION}">)
end
elem_plaintext = decrypt_node_with_multiple_keys(encrypted_node, decryption_keys)
# If we get some problematic noise in the plaintext after decrypting.
# This quick regexp parse will grab only the Element and discard the noise.
elem_plaintext = elem_plaintext.match(regexp)[0]
# To avoid namespace errors if saml namespace is not defined
# create a parent node first with the namespace defined
elem_plaintext = "#{node_header}#{elem_plaintext}</node>"
doc = Nokogiri::XML(elem_plaintext)
if encrypted_node.name == 'EncryptedAttribute'
doc.root.at_xpath('saml:Attribute', 'saml' => RubySaml::XML::NS_ASSERTION)
else
doc.root.children.first
end
end
private
def validate_decryption_keys!(decryption_keys)
decryption_keys = Array(decryption_keys)
if !decryption_keys || decryption_keys.empty?
# TODO: More generic error?
raise RubySaml::ValidationError.new('An encrypted element was found, but the Settings does not contain any SP private keys to decrypt it')
elsif decryption_keys.none?(OpenSSL::PKey::RSA)
raise RubySaml::ValidationError.new('SP encryption private keys must be OpenSSL::PKey::RSA keys')
end
end
# Obtains the decrypted string from an Encrypted node element in XML,
# given multiple private keys to try.
# @param encrypted_node [Nokogiri::XML::Element] The Encrypted element
# @param decryption_keys [OpenSSL::PKey::RSA | Array<OpenSSL::PKey::RSA>] The SP private key(s)
# @return [String] The decrypted data
def decrypt_node_with_multiple_keys(encrypted_node, decryption_keys)
error = nil
Array(decryption_keys).each do |key|
return decrypt_node_with_single_key(encrypted_node, key)
rescue OpenSSL::PKey::PKeyError => e
error ||= e
end
raise(error) if error
end
# Obtains the decrypted string from an Encrypted node element in XML
# @param encrypted_node [Nokogiri::XML::Element] The Encrypted element
# @param private_key [OpenSSL::PKey::RSA] The SP private key
# @return [String] The decrypted data
def decrypt_node_with_single_key(encrypted_node, private_key)
encrypt_data = encrypted_node.at_xpath(
'./xenc:EncryptedData',
{ 'xenc' => RubySaml::XML::XENC }
)
symmetric_key = retrieve_symmetric_key(encrypt_data, private_key)
cipher_value = encrypt_data.at_xpath(
'./xenc:CipherData/xenc:CipherValue',
{ 'xenc' => RubySaml::XML::XENC }
)
node = Base64.decode64(cipher_value.text)
encrypt_method = encrypt_data.at_xpath(
'./xenc:EncryptionMethod',
{ 'xenc' => RubySaml::XML::XENC }
)
algorithm = encrypt_method['Algorithm']
retrieve_plaintext(node, symmetric_key, algorithm)
end
# Obtains the symmetric key from the EncryptedData element
# @param encrypt_data [Nokogiri::XML::Element] The EncryptedData element
# @param private_key [OpenSSL::PKey::RSA] The SP private key
# @return [String] The symmetric key
def retrieve_symmetric_key(encrypt_data, private_key)
key_ref = retrieve_symmetric_key_reference(encrypt_data)
encrypted_key = encrypt_data.at_xpath(
"./ds:KeyInfo/xenc:EncryptedKey | ./KeyInfo/xenc:EncryptedKey#{' | //xenc:EncryptedKey[@Id=$id]' if key_ref}",
{ 'ds' => DSIG, 'xenc' => RubySaml::XML::XENC },
{ 'id' => key_ref }.compact
)
encrypted_symmetric_key_element = encrypted_key.at_xpath(
'./xenc:CipherData/xenc:CipherValue',
'xenc' => RubySaml::XML::XENC
)
cipher_text = Base64.decode64(encrypted_symmetric_key_element.text)
encrypt_method = encrypted_key.at_xpath(
'./xenc:EncryptionMethod',
'xenc' => RubySaml::XML::XENC
)
algorithm = encrypt_method['Algorithm']
retrieve_plaintext(cipher_text, private_key, algorithm)
end
def retrieve_symmetric_key_reference(encrypt_data)
reference = encrypt_data.at_xpath(
'./ds:KeyInfo/ds:RetrievalMethod/@URI',
{ 'ds' => DSIG }
)
reference = reference&.value&.delete_prefix('#')
reference unless reference&.empty?
end
# Obtains the deciphered text
# @param cipher_text [String] The ciphered text
# @param symmetric_key [String] The symmetric key used to encrypt the text
# @param algorithm [String] The encrypted algorithm
# @return [String] The deciphered text
def retrieve_plaintext(cipher_text, symmetric_key, algorithm)
case algorithm
when 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc' then cipher = OpenSSL::Cipher.new('DES-EDE3-CBC').decrypt
when 'http://www.w3.org/2001/04/xmlenc#aes128-cbc' then cipher = OpenSSL::Cipher.new('AES-128-CBC').decrypt
when 'http://www.w3.org/2001/04/xmlenc#aes192-cbc' then cipher = OpenSSL::Cipher.new('AES-192-CBC').decrypt
when 'http://www.w3.org/2001/04/xmlenc#aes256-cbc' then cipher = OpenSSL::Cipher.new('AES-256-CBC').decrypt
when 'http://www.w3.org/2009/xmlenc11#aes128-gcm' then auth_cipher = OpenSSL::Cipher.new('aes-128-gcm').decrypt
when 'http://www.w3.org/2009/xmlenc11#aes192-gcm' then auth_cipher = OpenSSL::Cipher.new('aes-192-gcm').decrypt
when 'http://www.w3.org/2009/xmlenc11#aes256-gcm' then auth_cipher = OpenSSL::Cipher.new('aes-256-gcm').decrypt
when 'http://www.w3.org/2001/04/xmlenc#rsa-1_5' then rsa = symmetric_key
when 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p' then oaep = symmetric_key
end
if cipher
iv_len = cipher.iv_len
data = cipher_text[iv_len..]
cipher.padding = 0
cipher.key = symmetric_key
cipher.iv = cipher_text[0..iv_len - 1]
assertion_plaintext = cipher.update(data)
assertion_plaintext << cipher.final
elsif auth_cipher
iv_len = auth_cipher.iv_len
text_len = cipher_text.length
tag_len = 16
data = cipher_text[iv_len..text_len - 1 - tag_len]
auth_cipher.padding = 0
auth_cipher.key = symmetric_key
auth_cipher.iv = cipher_text[0..iv_len - 1]
auth_cipher.auth_data = ''
auth_cipher.auth_tag = cipher_text[text_len - tag_len..]
assertion_plaintext = auth_cipher.update(data)
assertion_plaintext << auth_cipher.final
elsif rsa
rsa.private_decrypt(cipher_text)
elsif oaep
oaep.private_decrypt(cipher_text, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
else
cipher_text
end
end
end
end
end