main
  1# frozen_string_literal: true
  2
  3RSpec.describe Xml::Kit::Decryption do
  4  describe '#decrypt_hash' do
  5    let(:secret) { FFaker::Movie.title }
  6    let(:password) { FFaker::Movie.title }
  7
  8    context 'when decrypting AES-128-CBC data' do
  9      subject { described_class.new(private_keys: [private_key]) }
 10
 11      let(:key_pair) { generate_key_pair(password) }
 12      let(:certificate_pem) { key_pair[0] }
 13      let(:private_key_pem) { key_pair[1] }
 14      let(:public_key) { OpenSSL::X509::Certificate.new(certificate_pem).public_key }
 15      let(:private_key) { OpenSSL::PKey::RSA.new(private_key_pem, password) }
 16      let(:data) do
 17        cipher = OpenSSL::Cipher.new('AES-128-CBC')
 18        cipher.encrypt
 19        key = cipher.random_key
 20        iv = cipher.random_iv
 21        encrypted = cipher.update(secret) + cipher.final
 22        {
 23          'EncryptedData' => {
 24            'xmlns:xenc' => 'http://www.w3.org/2001/04/xmlenc#',
 25            'xmlns:dsig' => 'http://www.w3.org/2000/09/xmldsig#',
 26            'Type' => 'http://www.w3.org/2001/04/xmlenc#Element',
 27            'EncryptionMethod' => {
 28              'Algorithm' => 'http://www.w3.org/2001/04/xmlenc#aes128-cbc'
 29            },
 30            'KeyInfo' => {
 31              'xmlns:dsig' => 'http://www.w3.org/2000/09/xmldsig#',
 32              'EncryptedKey' => {
 33                'EncryptionMethod' => {
 34                  'Algorithm' => 'http://www.w3.org/2001/04/xmlenc#rsa-1_5'
 35                },
 36                'CipherData' => {
 37                  'CipherValue' => Base64.encode64(public_key.public_encrypt(key))
 38                }
 39              }
 40            },
 41            'CipherData' => {
 42              'CipherValue' => Base64.encode64(iv + encrypted)
 43            }
 44          }
 45        }
 46      end
 47
 48      specify { expect(subject.decrypt_hash(data).strip).to eql(secret) }
 49    end
 50
 51    context 'when multiple encryption keys are present' do
 52      subject { described_class.new(private_keys: [other_private_key, private_key]) }
 53
 54      let(:key_pair) { generate_key_pair(password) }
 55      let(:other_key_pair) { generate_key_pair(password) }
 56      let(:certificate_pem) { key_pair[0] }
 57      let(:private_key_pem) { key_pair[1] }
 58      let(:public_key) { OpenSSL::X509::Certificate.new(certificate_pem).public_key }
 59      let(:private_key) { OpenSSL::PKey::RSA.new(private_key_pem, password) }
 60      let(:other_private_key_pem) { other_key_pair[1] }
 61      let(:other_private_key) { OpenSSL::PKey::RSA.new(other_private_key_pem, password) }
 62
 63      let(:data) do
 64        cipher = OpenSSL::Cipher.new('AES-128-CBC')
 65        cipher.encrypt
 66        key = cipher.random_key
 67        iv = cipher.random_iv
 68        encrypted = cipher.update(secret) + cipher.final
 69
 70        {
 71          'EncryptedData' => {
 72            'xmlns:xenc' => 'http://www.w3.org/2001/04/xmlenc#',
 73            'xmlns:dsig' => 'http://www.w3.org/2000/09/xmldsig#',
 74            'Type' => 'http://www.w3.org/2001/04/xmlenc#Element',
 75            'EncryptionMethod' => {
 76              'Algorithm' => 'http://www.w3.org/2001/04/xmlenc#aes128-cbc'
 77            },
 78            'KeyInfo' => {
 79              'xmlns:dsig' => 'http://www.w3.org/2000/09/xmldsig#',
 80              'EncryptedKey' => {
 81                'EncryptionMethod' => {
 82                  'Algorithm' => 'http://www.w3.org/2001/04/xmlenc#rsa-1_5'
 83                },
 84                'CipherData' => {
 85                  'CipherValue' => Base64.encode64(public_key.public_encrypt(key))
 86                }
 87              }
 88            },
 89            'CipherData' => {
 90              'CipherValue' => Base64.encode64(iv + encrypted)
 91            }
 92          }
 93        }
 94      end
 95
 96      specify { expect(subject.decrypt_hash(data).strip).to eql(secret) }
 97    end
 98
 99    context 'when it cannot decrypt the data' do
100      subject { described_class.new(private_keys: [new_private_key]) }
101
102      let(:key_pair) { generate_key_pair(password) }
103      let(:certificate_pem) { key_pair[0] }
104      let(:public_key) { OpenSSL::X509::Certificate.new(certificate_pem).public_key }
105      let(:new_private_key_pem) { generate_key_pair(password)[1] }
106      let(:new_private_key) { OpenSSL::PKey::RSA.new(new_private_key_pem, password) }
107      let(:data) do
108        cipher = OpenSSL::Cipher.new('AES-128-CBC')
109        cipher.encrypt
110        key = cipher.random_key
111        iv = cipher.random_iv
112        encrypted = cipher.update(secret) + cipher.final
113
114        {
115          'EncryptedData' => {
116            'xmlns:xenc' => 'http://www.w3.org/2001/04/xmlenc#',
117            'xmlns:dsig' => 'http://www.w3.org/2000/09/xmldsig#',
118            'Type' => 'http://www.w3.org/2001/04/xmlenc#Element',
119            'EncryptionMethod' => {
120              'Algorithm' => 'http://www.w3.org/2001/04/xmlenc#aes128-cbc'
121            },
122            'KeyInfo' => {
123              'xmlns:dsig' => 'http://www.w3.org/2000/09/xmldsig#',
124              'EncryptedKey' => {
125                'EncryptionMethod' => {
126                  'Algorithm' => 'http://www.w3.org/2001/04/xmlenc#rsa-1_5'
127                },
128                'CipherData' => {
129                  'CipherValue' => Base64.encode64(public_key.public_encrypt(key))
130                }
131              }
132            },
133            'CipherData' => {
134              'CipherValue' => Base64.encode64(iv + encrypted)
135            }
136          }
137        }
138      end
139
140      specify { expect { subject.decrypt_hash(data) }.to raise_error(OpenSSL::PKey::RSAError) }
141    end
142  end
143
144  describe '#decrypt_document' do
145    subject { described_class.new(private_keys: [item.encryption_key_pair.private_key]) }
146
147    let(:item) { Item.new }
148    let(:document) { Nokogiri::XML(item.to_xml) }
149    let(:encrypted_node) { document.at_xpath('/x:Item/x:Encrypted/xmlenc:EncryptedData', 'xmlenc' => 'http://www.w3.org/2001/04/xmlenc#', x: 'https://www.example.org/item#') }
150
151    specify { expect(subject.decrypt_node(encrypted_node).name).to eql('EncryptMe') }
152    specify { expect(subject.decrypt_node(nil)).to be_nil }
153
154    context 'when it does not contain an EncryptedData' do
155      let(:document) { Nokogiri::XML('<hello><world></world></hello>') }
156      let(:node) { document.at_xpath('//hello/world') }
157
158      specify { expect(subject.decrypt_node(node)).to eql(node) }
159    end
160
161    context 'when the document cannot be decrypted' do
162      subject { described_class.new(private_keys: []) }
163
164      specify { expect { subject.decrypt_node(encrypted_node) }.to raise_error(Xml::Kit::DecryptionError) }
165    end
166
167    context 'when using the ruby-saml example' do
168      subject { described_class.new(cipher_registry: cipher_registry, private_keys: private_keys) }
169
170      let(:private_keys) { [OpenSSL::PKey::RSA.new(private_key_pem)] }
171      let(:private_key_pem) { IO.read('./spec/fixtures/private.txt') }
172      let(:document) { Nokogiri::XML(raw_xml) }
173      let(:encoded) { 'PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgSUQ9InBmeDUxOWI1Y2JiLWNiNmYtOTQzNS0xNjNmLWJkMzVjZTM1YzNmMCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDYtMDRUMDI6MjI6MDJaIiBEZXN0aW5hdGlvbj0iaHR0cDovL2FwcC5tdWRhLm5vL3Nzby9jb25zdW1lIiBJblJlc3BvbnNlVG89Il9mYzRhMzRiMC03ZWZiLTAxMmUtY2FhZS03ODJiY2IxM2JiMzgiPjxzYW1sOklzc3Vlcj5odHRwczovL2FwcC5vbmVsb2dpbi5jb20vc2FtbDI8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4NCiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZng1MTliNWNiYi1jYjZmLTk0MzUtMTYzZi1iZDM1Y2UzNWMzZjAiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPjBTRFRrNXNYWjdoMW9YUWVRMm5YY3BLZnZoTT08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+WENPVmk4U2c0MllRS25oMWpNTWYvV0dVcDh5Q1dFQWV4UE5taVNWT0M2dUFBUGc5WWwySUt4Um1SeGczcHpVK0o5SzlTRUVEOEJWenJERTZ4VDlxV1JUbXZ1WExqemE0TndvRmFGWllIc3ZzN0FPR3l5UEJjT3Z2R3JoM2RGWmVTUzF5U2tVc3FBWW5Wck54emRkRVFZa2trRmNxQkNqZ3dnd0Z5Vlpvbkc4PTwvZHM6U2lnbmF0dXJlVmFsdWU+DQo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDR3pDQ0FZUUNDUUNOTmNRWG9tMzJWREFOQmdrcWhraUc5dzBCQVFVRkFEQlNNUXN3Q1FZRFZRUUdFd0pWVXpFTE1Ba0dBMVVFQ0JNQ1NVNHhGVEFUQmdOVkJBY1RERWx1WkdsaGJtRndiMnhwY3pFUk1BOEdBMVVFQ2hNSVQyNWxURzluYVc0eEREQUtCZ05WQkFzVEEwVnVaekFlRncweE5EQTBNak14T0RReE1ERmFGdzB4TlRBME1qTXhPRFF4TURGYU1GSXhDekFKQmdOVkJBWVRBbFZUTVFzd0NRWURWUVFJRXdKSlRqRVZNQk1HQTFVRUJ4TU1TVzVrYVdGdVlYQnZiR2x6TVJFd0R3WURWUVFLRXdoUGJtVk1iMmRwYmpFTU1Bb0dBMVVFQ3hNRFJXNW5NSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURvNm0rUVp2WVEveEwwRWxMZ3VwSzFRRGNZTDRmNVBja3dzTmdTOXBVdlY3ZnpUcUNIazhUaEx4VGs0Mk1RMk1jSnNPZVVKVlA3MjhLaHltakZDcXhnUDRWdXdSazlycEFsMCttaHk2TVBkeWp5QTZHMTRqckRXUzY1eXNMY2hLNHQvdndwRUR6MFNRbEVvRzFrTXpsbFNtN3paUzNYcmVnQTdEak5hVVlRcXdJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBTE0ydkdDaVEvdm0rYTZ2NDArVlgyemRxSEEyUS8xdkYxaWJReko1NE1KQ09WV3ZzK3ZRWGZaRmhkbTBPUE0ySXJEVTdvcXZLUHFQNnhPQWVKSzZIMHlQN000WUwzZmF0U3ZJWW1tZnlYQzlrdDNTdnovTnlySHpQaFVuSjB5ZS9zVVNYeG56UXh3Y20vOVB3QXFyUWFBM1FwUWtINTd5YkYvT29yeVBlKzJoPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgVmVyc2lvbj0iMi4wIiBJRD0icGZ4OTUxNmIwZjMtNDUzNi0xMGY2LWM2ZmEtOWRkNTIzZTE0OThjIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDYtMDRUMDI6MjI6MDJaIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9hcHAub25lbG9naW4uY29tL3NhbWwyPC9zYW1sOklzc3Vlcj48c2FtbDpTdWJqZWN0PjxzYW1sOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBOb3RPbk9yQWZ0ZXI9IjIwMzAtMDYtMDRUMDI6Mjc6MDJaIiBSZWNpcGllbnQ9InJlY2lwaWVudCIvPjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPjxzYW1sOkVuY3J5cHRlZElEPjx4ZW5jOkVuY3J5cHRlZERhdGEgeG1sbnM6eGVuYz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjIiB4bWxuczpkc2lnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIiBUeXBlPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNFbGVtZW50Ij48eGVuYzpFbmNyeXB0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjYWVzMTI4LWNiYyIvPjxkc2lnOktleUluZm8geG1sbnM6ZHNpZz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+PHhlbmM6RW5jcnlwdGVkS2V5Pjx4ZW5jOkVuY3J5cHRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNyc2EtMV81Ii8+PHhlbmM6Q2lwaGVyRGF0YT48eGVuYzpDaXBoZXJWYWx1ZT5ZUkdFZGF2dWpSNlYwNUZsWERHbmxCK1VUWTFjak9DallkYlhKN2JBZHFURWxDTyt1eHl0aytnMWVTTGVuczhJcjlZaVBNNUorUWU5cXo0TkdORXdyNjV6aDM5L0ZJVXNMQ3BhaXQ3QjZXM2lFcmR4aVUrSUN1cUw3TCtNSmlGVHZiVG90NVdleWZvVkFnSE94Z1BodDRONlZSL3BhYzRDdFZEQ0ZBbDlEMjA9PC94ZW5jOkNpcGhlclZhbHVlPjwveGVuYzpDaXBoZXJEYXRhPjwveGVuYzpFbmNyeXB0ZWRLZXk+PC9kc2lnOktleUluZm8+DQogICA8eGVuYzpDaXBoZXJEYXRhPg0KICAgICAgPHhlbmM6Q2lwaGVyVmFsdWU+dFdQZEV1dXZmSjh3WVBhOFVUQTRvR2htRENQTjFhQzVkUUFEN0g5SkhWQm5VS3Y0UkljNEQ3SnVJem12bXlyalZGWmRGNW15K3cvUGd3dWlOVGdpOUxid01iSW5adW1HbDhlSndFblBaVXBPQ0w1dDNXbEdKbU85OVVNejZQUVNLeGlGSU1DYzcrQXlRQmpjdTEzaUxWeU5TbFQyWDMxRXBOaW5jQ3FzSldvPTwveGVuYzpDaXBoZXJWYWx1ZT4NCiAgIDwveGVuYzpDaXBoZXJEYXRhPg0KPC94ZW5jOkVuY3J5cHRlZERhdGE+PC9zYW1sOkVuY3J5cHRlZElEPjwvc2FtbDpTdWJqZWN0PjxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDExLTA2LTA0VDAyOjE3OjAyWiIgTm90T25PckFmdGVyPSIyMDMwLTA2LTA0VDAyOjI3OjAyWiI+PHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDpBdWRpZW5jZT5odHRwczovL3NvbWVvbmUuZXhhbXBsZS5jb20vYXVkaWVuY2U8L3NhbWw6QXVkaWVuY2U+PC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PC9zYW1sOkNvbmRpdGlvbnM+PHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDE0LTA2LTA0VDAyOjIyOjAyWiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjAzMC0wNi0wNVQwMjoyMjowMloiIFNlc3Npb25JbmRleD0iXzE2ZjU3MGZiYzAzMTUwMDdhMDM1NWRmZWE2YjNjNDZjIj48c2FtbDpBdXRobkNvbnRleHQ+PHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmRQcm90ZWN0ZWRUcmFuc3BvcnQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+PC9zYW1sOkF1dGhuQ29udGV4dD48L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg==' }
174      let(:raw_xml) { Base64.decode64(encoded) }
175      let(:encrypted_node) do
176        document.at_xpath(
177          '/samlp:Response/saml:Assertion/saml:Subject/saml:EncryptedID/xmlenc:EncryptedData',
178          'xmlenc' => 'http://www.w3.org/2001/04/xmlenc#',
179          'saml' => 'urn:oasis:names:tc:SAML:2.0:assertion',
180          'samlp' => 'urn:oasis:names:tc:SAML:2.0:protocol'
181        )
182      end
183      let(:cipher_registry) do
184        Xml::Kit::Crypto.cipher_registry do |algorithm, key|
185          if algorithm == 'http://www.w3.org/2001/04/xmlenc#aes128-cbc'
186            Xml::Kit::Crypto::SymmetricCipher.new(algorithm, key, 0)
187          else
188            Xml::Kit::Crypto.cipher_for(algorithm, key)
189          end
190        end
191      end
192
193      specify do
194        expect(subject.decrypt_node(encrypted_node).to_s).to eql('<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">test@onelogin.com</saml:NameID>')
195      end
196    end
197  end
198end