Commit 042cf81

mo <mo.khan@gmail.com>
2017-12-26 20:43:31
move xml decryption to xml-kit.
1 parent 1bbacda
lib/xml/kit/crypto/oaep_cipher.rb
@@ -0,0 +1,22 @@
+module Xml
+  module Kit
+    module Crypto
+      class OaepCipher
+        ALGORITHMS = {
+          'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p' => true,
+        }
+        def initialize(algorithm, key)
+          @key = key
+        end
+
+        def self.matches?(algorithm)
+          ALGORITHMS[algorithm]
+        end
+
+        def decrypt(cipher_text)
+          @key.private_decrypt(cipher_text, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
+        end
+      end
+    end
+  end
+end
lib/xml/kit/crypto/rsa_cipher.rb
@@ -0,0 +1,23 @@
+module Xml
+  module Kit
+    module Crypto
+      class RsaCipher
+        ALGORITHMS = {
+          'http://www.w3.org/2001/04/xmlenc#rsa-1_5' => true,
+        }
+
+        def initialize(algorithm, key)
+          @key = key
+        end
+
+        def self.matches?(algorithm)
+          ALGORITHMS[algorithm]
+        end
+
+        def decrypt(cipher_text)
+          @key.private_decrypt(cipher_text)
+        end
+      end
+    end
+  end
+end
lib/xml/kit/crypto/simple_cipher.rb
@@ -0,0 +1,38 @@
+module Xml
+  module Kit
+    module Crypto
+      class SimpleCipher
+        ALGORITHMS = {
+          'http://www.w3.org/2001/04/xmlenc#tripledes-cbc' => 'DES-EDE3-CBC',
+          'http://www.w3.org/2001/04/xmlenc#aes128-cbc' => 'AES-128-CBC',
+          'http://www.w3.org/2001/04/xmlenc#aes192-cbc' => 'AES-192-CBC',
+          'http://www.w3.org/2001/04/xmlenc#aes256-cbc' => 'AES-256-CBC',
+        }
+
+        def initialize(algorithm, private_key)
+          @algorithm = algorithm
+          @private_key = private_key
+        end
+
+        def self.matches?(algorithm)
+          ALGORITHMS[algorithm]
+        end
+
+        def decrypt(cipher_text)
+          cipher = OpenSSL::Cipher.new(ALGORITHMS[@algorithm])
+          cipher.decrypt
+          iv = cipher_text[0..cipher.iv_len-1]
+          data = cipher_text[cipher.iv_len..-1]
+          #cipher.padding = 0
+          cipher.key = @private_key
+          cipher.iv = iv
+
+          Saml::Kit.logger.debug ['-key', @private_key].inspect
+          Saml::Kit.logger.debug ['-iv', iv].inspect
+
+          cipher.update(data) + cipher.final
+        end
+      end
+    end
+  end
+end
lib/xml/kit/crypto/unknown_cipher.rb
@@ -0,0 +1,18 @@
+module Xml
+  module Kit
+    module Crypto
+      class UnknownCipher
+        def initialize(algorithm, key)
+        end
+
+        def self.matches?(algorithm)
+          true
+        end
+
+        def decrypt(cipher_text)
+          cipher_text
+        end
+      end
+    end
+  end
+end
lib/xml/kit/crypto.rb
@@ -0,0 +1,17 @@
+require 'xml/kit/crypto/oaep_cipher'
+require 'xml/kit/crypto/rsa_cipher'
+require 'xml/kit/crypto/simple_cipher'
+require 'xml/kit/crypto/unknown_cipher'
+
+module Xml
+  module Kit
+    module Crypto
+      DECRYPTORS = [ SimpleCipher, RsaCipher, OaepCipher, UnknownCipher ]
+
+      # @!visibility private
+      def self.decryptor_for(algorithm, key)
+        DECRYPTORS.find { |x| x.matches?(algorithm) }.new(algorithm, key)
+      end
+    end
+  end
+end
lib/xml/kit/xml_decryption.rb
@@ -0,0 +1,44 @@
+module Xml
+  module Kit
+    # {include:file:spec/saml/xml_decryption_spec.rb}
+    class XmlDecryption
+      # The list of private keys to use to attempt to decrypt the document.
+      attr_reader :private_keys
+
+      def initialize(configuration: Saml::Kit.configuration)
+        @private_keys = configuration.private_keys(use: :encryption)
+      end
+
+      # Decrypts an EncryptedData section of an XML document.
+      #
+      # @param data [Hash] the XML document converted to a [Hash] using Hash.from_xml.
+      def decrypt(data)
+        encrypted_data = data['EncryptedData']
+        symmetric_key = symmetric_key_from(encrypted_data)
+        cipher_text = Base64.decode64(encrypted_data["CipherData"]["CipherValue"])
+        to_plaintext(cipher_text, symmetric_key, encrypted_data["EncryptionMethod"]['Algorithm'])
+      end
+
+      private
+
+      def symmetric_key_from(encrypted_data)
+        encrypted_key = encrypted_data['KeyInfo']['EncryptedKey']
+        cipher_text = Base64.decode64(encrypted_key['CipherData']['CipherValue'])
+        attempts = private_keys.count
+        private_keys.each do |private_key|
+          begin
+            attempts -= 1
+            return to_plaintext(cipher_text, private_key, encrypted_key["EncryptionMethod"]['Algorithm'])
+          rescue OpenSSL::PKey::RSAError => error
+            Saml::Kit.logger.error(error)
+            raise if attempts.zero?
+          end
+        end
+      end
+
+      def to_plaintext(cipher_text, symmetric_key, algorithm)
+        Crypto.decryptor_for(algorithm, symmetric_key).decrypt(cipher_text)
+      end
+    end
+  end
+end
lib/xml/kit.rb
@@ -1,7 +1,9 @@
 require "xml/kit/version"
 
+require "xml/kit/crypto"
+require "xml/kit/xml_decryption"
+
 module Xml
   module Kit
-    # Your code goes here...
   end
 end
spec/xml/xml_decryption_spec.rb
@@ -0,0 +1,133 @@
+RSpec.describe Xml::Kit::XmlDecryption do
+  describe "#decrypt" do
+    let(:secret) { FFaker::Movie.title }
+    let(:password) { FFaker::Movie.title }
+
+    it 'decrypts the data' do
+      certificate_pem, private_key_pem = Saml::Kit::SelfSignedCertificate.new(password).create
+      public_key = OpenSSL::X509::Certificate.new(certificate_pem).public_key
+      private_key = OpenSSL::PKey::RSA.new(private_key_pem, password)
+
+      cipher = OpenSSL::Cipher.new('AES-128-CBC')
+      cipher.encrypt
+      key = cipher.random_key
+      iv = cipher.random_iv
+      encrypted = cipher.update(secret) + cipher.final
+
+      data = {
+        "EncryptedData"=> {
+          "xmlns:xenc"=>"http://www.w3.org/2001/04/xmlenc#",
+          "xmlns:dsig"=>"http://www.w3.org/2000/09/xmldsig#",
+          "Type"=>"http://www.w3.org/2001/04/xmlenc#Element",
+          "EncryptionMethod"=> {
+            "Algorithm"=>"http://www.w3.org/2001/04/xmlenc#aes128-cbc"
+          },
+          "KeyInfo"=> {
+            "xmlns:dsig"=>"http://www.w3.org/2000/09/xmldsig#",
+            "EncryptedKey"=> {
+              "EncryptionMethod"=>{
+                "Algorithm"=>"http://www.w3.org/2001/04/xmlenc#rsa-1_5"
+              },
+              "CipherData"=>{
+                "CipherValue"=> Base64.encode64(public_key.public_encrypt(key))
+              }
+            }
+          },
+          "CipherData"=>{
+            "CipherValue"=> Base64.encode64(iv + encrypted)
+          }
+        }
+      }
+      subject = described_class.new(configuration: double(private_keys: [private_key]))
+      decrypted = subject.decrypt(data)
+      expect(decrypted.strip).to eql(secret)
+    end
+
+    it 'attemps to decrypt with each encryption keypair' do
+      certificate_pem, private_key_pem = Saml::Kit::SelfSignedCertificate.new(password).create
+      public_key = OpenSSL::X509::Certificate.new(certificate_pem).public_key
+      private_key = OpenSSL::PKey::RSA.new(private_key_pem, password)
+
+      cipher = OpenSSL::Cipher.new('AES-128-CBC')
+      cipher.encrypt
+      key = cipher.random_key
+      iv = cipher.random_iv
+      encrypted = cipher.update(secret) + cipher.final
+
+      data = {
+        "EncryptedData"=> {
+          "xmlns:xenc"=>"http://www.w3.org/2001/04/xmlenc#",
+          "xmlns:dsig"=>"http://www.w3.org/2000/09/xmldsig#",
+          "Type"=>"http://www.w3.org/2001/04/xmlenc#Element",
+          "EncryptionMethod"=> {
+            "Algorithm"=>"http://www.w3.org/2001/04/xmlenc#aes128-cbc"
+          },
+          "KeyInfo"=> {
+            "xmlns:dsig"=>"http://www.w3.org/2000/09/xmldsig#",
+            "EncryptedKey"=> {
+              "EncryptionMethod"=>{
+                "Algorithm"=>"http://www.w3.org/2001/04/xmlenc#rsa-1_5"
+              },
+              "CipherData"=>{
+                "CipherValue"=> Base64.encode64(public_key.public_encrypt(key))
+              }
+            }
+          },
+          "CipherData"=>{
+            "CipherValue"=> Base64.encode64(iv + encrypted)
+          }
+        }
+      }
+
+      _, other_private_key_pem = Saml::Kit::SelfSignedCertificate.new(password).create
+      other_private_key = OpenSSL::PKey::RSA.new(other_private_key_pem, password)
+
+      subject = described_class.new(configuration: double(private_keys: [other_private_key, private_key]))
+      decrypted = subject.decrypt(data)
+      expect(decrypted.strip).to eql(secret)
+    end
+
+    it 'raise an error when it cannot decrypt the data' do
+      certificate_pem, _ = Saml::Kit::SelfSignedCertificate.new(password).create
+      public_key = OpenSSL::X509::Certificate.new(certificate_pem).public_key
+
+      cipher = OpenSSL::Cipher.new('AES-128-CBC')
+      cipher.encrypt
+      key = cipher.random_key
+      iv = cipher.random_iv
+      encrypted = cipher.update(secret) + cipher.final
+
+      data = {
+        "EncryptedData"=> {
+          "xmlns:xenc"=>"http://www.w3.org/2001/04/xmlenc#",
+          "xmlns:dsig"=>"http://www.w3.org/2000/09/xmldsig#",
+          "Type"=>"http://www.w3.org/2001/04/xmlenc#Element",
+          "EncryptionMethod"=> {
+            "Algorithm"=>"http://www.w3.org/2001/04/xmlenc#aes128-cbc"
+          },
+          "KeyInfo"=> {
+            "xmlns:dsig"=>"http://www.w3.org/2000/09/xmldsig#",
+            "EncryptedKey"=> {
+              "EncryptionMethod"=>{
+                "Algorithm"=>"http://www.w3.org/2001/04/xmlenc#rsa-1_5"
+              },
+              "CipherData"=>{
+                "CipherValue"=> Base64.encode64(public_key.public_encrypt(key))
+              }
+            }
+          },
+          "CipherData"=>{
+            "CipherValue"=> Base64.encode64(iv + encrypted)
+          }
+        }
+      }
+
+      new_private_key_pem = Saml::Kit::SelfSignedCertificate.new(password).create[1]
+      new_private_key = OpenSSL::PKey::RSA.new(new_private_key_pem, password)
+      subject = described_class.new(configuration: double(private_keys: [new_private_key]))
+      expect do
+        subject.decrypt(data)
+      end.to raise_error(OpenSSL::PKey::RSAError)
+    end
+  end
+end