sepafm 0.1.5 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.yardopts +6 -0
- data/lib/sepa/application_request.rb +87 -0
- data/lib/sepa/application_response.rb +37 -2
- data/lib/sepa/attribute_checks.rb +30 -0
- data/lib/sepa/banks/danske/danske_response.rb +86 -0
- data/lib/sepa/banks/danske/soap_danske.rb +81 -3
- data/lib/sepa/banks/nordea/nordea_response.rb +18 -0
- data/lib/sepa/banks/nordea/soap_nordea.rb +25 -2
- data/lib/sepa/client.rb +203 -18
- data/lib/sepa/error_messages.rb +54 -13
- data/lib/sepa/response.rb +118 -11
- data/lib/sepa/soap_builder.rb +53 -2
- data/lib/sepa/utilities.rb +167 -6
- data/lib/sepa/version.rb +3 -1
- data/lib/sepafm.rb +57 -4
- data/readme.md +74 -60
- data/test/sepa/sepa_test.rb +1 -1
- metadata +5 -3
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA1:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 3071e7c2f8f6ab6f0fcff615a38f2d11517952da
         | 
| 4 | 
            +
              data.tar.gz: fc8beef75e2f56ece35cccbd5101d1d2cc9af062
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 734914613e29893e3c2188a0a3e390bbaf0ae0c170ef58a045f40261e3691c49fa9fc560753c2f5db83a0bd0ff013aab936d889746bc462d0b99571538ea318d
         | 
| 7 | 
            +
              data.tar.gz: f0d4fac6bbb809d2750f2f6f501e5467398dd85ecef9ab8abbd6a2f6788734284ca12b0ebf066b0e87fc36637c1911c6ad2d1ec53a992abefcd4c110241c83a0
         | 
    
        data/.yardopts
    ADDED
    
    
| @@ -1,7 +1,21 @@ | |
| 1 1 | 
             
            module Sepa
         | 
| 2 | 
            +
             | 
| 3 | 
            +
              # Contains functionality to build the application request
         | 
| 4 | 
            +
              #
         | 
| 5 | 
            +
              # @todo Add return values for content modifying methods to signal whether they succeeded or not
         | 
| 2 6 | 
             
              class ApplicationRequest
         | 
| 3 7 | 
             
                include Utilities
         | 
| 4 8 |  | 
| 9 | 
            +
                # Initializes the {ApplicationRequest} with a params hash. The application request is usually
         | 
| 10 | 
            +
                # initialized by the {SoapBuilder}. The xml template of the application request is also loaded
         | 
| 11 | 
            +
                # here.
         | 
| 12 | 
            +
                #
         | 
| 13 | 
            +
                # @param params [Hash] the hash containing attributes needed by the {ApplicationRequest}. All
         | 
| 14 | 
            +
                #   the key => value pairs in the hash are initialized as instance variables. The hash in the
         | 
| 15 | 
            +
                #   initialization is usually the same as with {SoapBuilder} so the values have already been
         | 
| 16 | 
            +
                #   validated by the client.
         | 
| 17 | 
            +
                # @todo Consider not using instance_variable_set so that all the available instance variables
         | 
| 18 | 
            +
                #   can easily be seen.
         | 
| 5 19 | 
             
                def initialize(params = {})
         | 
| 6 20 | 
             
                  # Set all params as instance variables
         | 
| 7 21 | 
             
                  params.each do |key, value|
         | 
| @@ -11,6 +25,11 @@ module Sepa | |
| 11 25 | 
             
                  @application_request = load_body_template AR_TEMPLATE_PATH
         | 
| 12 26 | 
             
                end
         | 
| 13 27 |  | 
| 28 | 
            +
                # Sets the nodes in the application request, processes signature and then returns the
         | 
| 29 | 
            +
                # application request as an xml document.
         | 
| 30 | 
            +
                #
         | 
| 31 | 
            +
                # @return [String] the application request as an xml document
         | 
| 32 | 
            +
                # @todo This method is obviously doing too much
         | 
| 14 33 | 
             
                def to_xml
         | 
| 15 34 | 
             
                  set_common_nodes
         | 
| 16 35 | 
             
                  set_nodes_contents
         | 
| @@ -18,28 +37,48 @@ module Sepa | |
| 18 37 | 
             
                  @application_request.to_xml
         | 
| 19 38 | 
             
                end
         | 
| 20 39 |  | 
| 40 | 
            +
                # Base64 encodes the whole application request
         | 
| 41 | 
            +
                #
         | 
| 42 | 
            +
                # @return [String] the base64 encoded application request
         | 
| 21 43 | 
             
                def to_base64
         | 
| 22 44 | 
             
                  encode to_xml
         | 
| 23 45 | 
             
                end
         | 
| 24 46 |  | 
| 47 | 
            +
                # Returns the application request as a Nokogiri document
         | 
| 48 | 
            +
                #
         | 
| 49 | 
            +
                # @return [Nokogiri::XML::Document] the application request as a nokogiri document
         | 
| 25 50 | 
             
                def to_nokogiri
         | 
| 26 51 | 
             
                  Nokogiri::XML to_xml
         | 
| 27 52 | 
             
                end
         | 
| 28 53 |  | 
| 29 54 | 
             
                private
         | 
| 30 55 |  | 
| 56 | 
            +
                  # Sets node to value
         | 
| 57 | 
            +
                  #
         | 
| 58 | 
            +
                  # @param node [String] the name of the node which value is to be set
         | 
| 59 | 
            +
                  # @param value [#to_s] the value which is going to be set to the node
         | 
| 31 60 | 
             
                  def set_node(node, value)
         | 
| 32 61 | 
             
                    @application_request.at_css(node).content = value
         | 
| 33 62 | 
             
                  end
         | 
| 34 63 |  | 
| 64 | 
            +
                  # Sets node to base64 encoded value
         | 
| 65 | 
            +
                  #
         | 
| 66 | 
            +
                  # @param node [String] name of the node
         | 
| 67 | 
            +
                  # @param value [#to_s] the value which is going to be set to the nodea base64 encoded
         | 
| 68 | 
            +
                  # @todo rename
         | 
| 35 69 | 
             
                  def set_node_b(node, value)
         | 
| 36 70 | 
             
                    set_node node, encode(value)
         | 
| 37 71 | 
             
                  end
         | 
| 38 72 |  | 
| 73 | 
            +
                  # Converts {#command} to string, removes underscores and capitalizes it.
         | 
| 74 | 
            +
                  #
         | 
| 75 | 
            +
                  # @example Example input and output
         | 
| 76 | 
            +
                  #   :get_user_info --> GetUserInfo
         | 
| 39 77 | 
             
                  def pretty_command
         | 
| 40 78 | 
             
                    @command.to_s.split(/[\W_]/).map {|c| c.capitalize}.join
         | 
| 41 79 | 
             
                  end
         | 
| 42 80 |  | 
| 81 | 
            +
                  # Determines which content setting method to call depending on {#command}
         | 
| 43 82 | 
             
                  def set_nodes_contents
         | 
| 44 83 | 
             
                    case @command
         | 
| 45 84 | 
             
                    when :create_certificate
         | 
| @@ -57,6 +96,7 @@ module Sepa | |
| 57 96 | 
             
                    end
         | 
| 58 97 | 
             
                  end
         | 
| 59 98 |  | 
| 99 | 
            +
                  # Sets nodes' values for download file request
         | 
| 60 100 | 
             
                  def set_download_file_nodes
         | 
| 61 101 | 
             
                    add_target_id_after 'FileReferences'
         | 
| 62 102 | 
             
                    set_node("Status", @status)
         | 
| @@ -64,6 +104,11 @@ module Sepa | |
| 64 104 | 
             
                    set_node("FileReference", @file_reference)
         | 
| 65 105 | 
             
                  end
         | 
| 66 106 |  | 
| 107 | 
            +
                  # Sets Danske Bank's get bank certificate request's contents
         | 
| 108 | 
            +
                  #
         | 
| 109 | 
            +
                  # @raise [OnlyWorksWithDanske] if {#bank} is not danske
         | 
| 110 | 
            +
                  # @todo Investigate a better way to set the bank's root certificate's serial instead of
         | 
| 111 | 
            +
                  #   hardcoding it
         | 
| 67 112 | 
             
                  def set_get_bank_certificate_nodes
         | 
| 68 113 | 
             
                    raise 'OnlyWorksWithDanske' if @bank != :danske
         | 
| 69 114 |  | 
| @@ -73,24 +118,34 @@ module Sepa | |
| 73 118 | 
             
                    set_node("elem|RequestId", @request_id)
         | 
| 74 119 | 
             
                  end
         | 
| 75 120 |  | 
| 121 | 
            +
                  # Sets nodes' contents for upload file request
         | 
| 76 122 | 
             
                  def set_upload_file_nodes
         | 
| 77 123 | 
             
                    set_node_b("Content", @content)
         | 
| 78 124 | 
             
                    set_node("FileType", @file_type)
         | 
| 79 125 | 
             
                    add_target_id_after 'Environment'
         | 
| 80 126 | 
             
                  end
         | 
| 81 127 |  | 
| 128 | 
            +
                  # Sets nodes' contents for download file list request
         | 
| 82 129 | 
             
                  def set_download_file_list_nodes
         | 
| 83 130 | 
             
                    add_target_id_after 'Environment'
         | 
| 84 131 | 
             
                    set_node("Status", @status)
         | 
| 85 132 | 
             
                    set_node("FileType", @file_type)
         | 
| 86 133 | 
             
                  end
         | 
| 87 134 |  | 
| 135 | 
            +
                  # Sets nodes' contents for Nordea's get certificate request
         | 
| 136 | 
            +
                  #
         | 
| 137 | 
            +
                  # @todo Raise error if {#bank} is other than Nordea like in {#set_get_bank_certificate_nodes}
         | 
| 138 | 
            +
                  # @todo Check further into what service actually is
         | 
| 88 139 | 
             
                  def set_get_certificate_nodes
         | 
| 89 140 | 
             
                    set_node("Service", '')
         | 
| 90 141 | 
             
                    set_node("Content", format_cert_request(@signing_csr))
         | 
| 91 142 | 
             
                    set_node("HMAC", hmac(@pin, csr_to_binary(@signing_csr)))
         | 
| 92 143 | 
             
                  end
         | 
| 93 144 |  | 
| 145 | 
            +
                  # Sets nodes' contents for Danske Bank's create certificate request. Environment is set to
         | 
| 146 | 
            +
                  # customertest if {#environment} is `:test`
         | 
| 147 | 
            +
                  #
         | 
| 148 | 
            +
                  # @todo Raise error if {#bank} is other than Nordea like in {#set_get_bank_certificate_nodes}
         | 
| 94 149 | 
             
                  def set_create_certificate_nodes
         | 
| 95 150 | 
             
                    set_node("tns|CustomerId", @customer_id)
         | 
| 96 151 | 
             
                    set_node("tns|KeyGeneratorType", 'software')
         | 
| @@ -105,6 +160,8 @@ module Sepa | |
| 105 160 | 
             
                    set_node("tns|PIN", @pin)
         | 
| 106 161 | 
             
                  end
         | 
| 107 162 |  | 
| 163 | 
            +
                  # Sets contents for nodes that are common to all requests except when {#command} is
         | 
| 164 | 
            +
                  # `:get_bank_certificate` or `:create_certificate`. {#environment} is upcased here.
         | 
| 108 165 | 
             
                  def set_common_nodes
         | 
| 109 166 | 
             
                    return if @command == :get_bank_certificate
         | 
| 110 167 | 
             
                    return if @command == :create_certificate
         | 
| @@ -116,25 +173,48 @@ module Sepa | |
| 116 173 | 
             
                    set_node("Command", pretty_command)
         | 
| 117 174 | 
             
                  end
         | 
| 118 175 |  | 
| 176 | 
            +
                  # Removes a node from {#application_request}
         | 
| 177 | 
            +
                  #
         | 
| 178 | 
            +
                  # @param node [String] name of the node to remove
         | 
| 179 | 
            +
                  # @param xmlns [String] the namespace of the node
         | 
| 180 | 
            +
                  # @todo Move to {Utilities} and move document to parameters
         | 
| 119 181 | 
             
                  def remove_node(node, xmlns)
         | 
| 120 182 | 
             
                    @application_request.at_css("xmlns|#{node}", 'xmlns' => xmlns).remove
         | 
| 121 183 | 
             
                  end
         | 
| 122 184 |  | 
| 185 | 
            +
                  # Adds node to the root of the application request
         | 
| 186 | 
            +
                  #
         | 
| 187 | 
            +
                  # @todo Move to {Utilities} and move document to parameters
         | 
| 123 188 | 
             
                  def add_node_to_root(node)
         | 
| 124 189 | 
             
                    @application_request.root.add_child(node)
         | 
| 125 190 | 
             
                  end
         | 
| 126 191 |  | 
| 192 | 
            +
                  # Calculates the digest of {#application_request}
         | 
| 193 | 
            +
                  #
         | 
| 194 | 
            +
                  # @todo Use the digest calculation method in {Utilities} instead of implementing the
         | 
| 195 | 
            +
                  #   functionality again here.
         | 
| 196 | 
            +
                  # @return [String] the base64 encoded digest of the {#application_request}
         | 
| 127 197 | 
             
                  def calculate_digest
         | 
| 128 198 | 
             
                    sha1 = OpenSSL::Digest::SHA1.new
         | 
| 129 199 | 
             
                    encode(sha1.digest(@application_request.canonicalize))
         | 
| 130 200 | 
             
                  end
         | 
| 131 201 |  | 
| 202 | 
            +
                  # Adds value to signature node
         | 
| 203 | 
            +
                  #
         | 
| 204 | 
            +
                  # @param node [String] name of the signature node
         | 
| 205 | 
            +
                  # @param value [#to_s] the value to be set to the node
         | 
| 206 | 
            +
                  # @todo Remove this method and use {#set_node} method
         | 
| 132 207 | 
             
                  def add_value_to_signature(node, value)
         | 
| 133 208 | 
             
                    dsig = 'http://www.w3.org/2000/09/xmldsig#'
         | 
| 134 209 | 
             
                    sig = @application_request.at_css("dsig|#{node}", 'dsig' => dsig)
         | 
| 135 210 | 
             
                    sig.content = value
         | 
| 136 211 | 
             
                  end
         | 
| 137 212 |  | 
| 213 | 
            +
                  # Calculates the application request's signature value. Uses {#signing_private_key} for the
         | 
| 214 | 
            +
                  # calculation.
         | 
| 215 | 
            +
                  #
         | 
| 216 | 
            +
                  # @return [String] the base64 encoded signature
         | 
| 217 | 
            +
                  # @todo Move to {Utilities}
         | 
| 138 218 | 
             
                  def calculate_signature
         | 
| 139 219 | 
             
                    sha1 = OpenSSL::Digest::SHA1.new
         | 
| 140 220 | 
             
                    dsig = 'http://www.w3.org/2000/09/xmldsig#'
         | 
| @@ -143,6 +223,9 @@ module Sepa | |
| 143 223 | 
             
                    encode signature
         | 
| 144 224 | 
             
                  end
         | 
| 145 225 |  | 
| 226 | 
            +
                  # Removes signature from the application request, calculates the application request's digest,
         | 
| 227 | 
            +
                  # calculates the signature and adds needed values to signature node. Also adds
         | 
| 228 | 
            +
                  # {#own_signing_certificate} to the signature node.
         | 
| 146 229 | 
             
                  def process_signature
         | 
| 147 230 | 
             
                    # No signature for Certificate Requests
         | 
| 148 231 | 
             
                    return if @command == :get_certificate
         | 
| @@ -157,6 +240,10 @@ module Sepa | |
| 157 240 | 
             
                    add_value_to_signature('X509Certificate', format_cert(@own_signing_certificate))
         | 
| 158 241 | 
             
                  end
         | 
| 159 242 |  | 
| 243 | 
            +
                  # Adds target id to the application request after a specific node because the schema defines a
         | 
| 244 | 
            +
                  # sequence. Target id is only added if {#bank} is `:nordea`
         | 
| 245 | 
            +
                  #
         | 
| 246 | 
            +
                  # @param node [String] the name of the node after which the target id node will be added
         | 
| 160 247 | 
             
                  def add_target_id_after(node)
         | 
| 161 248 | 
             
                    return unless @bank == :nordea
         | 
| 162 249 |  | 
| @@ -1,22 +1,39 @@ | |
| 1 1 | 
             
            module Sepa
         | 
| 2 | 
            +
             | 
| 3 | 
            +
              # Contains functionality for the application response embedded in {Response}
         | 
| 4 | 
            +
              # @todo Use functionality from this class more when validating response
         | 
| 2 5 | 
             
              class ApplicationResponse
         | 
| 3 6 | 
             
                include ActiveModel::Validations
         | 
| 4 7 | 
             
                include Utilities
         | 
| 5 8 |  | 
| 9 | 
            +
                # The raw xml of the application response
         | 
| 10 | 
            +
                #
         | 
| 11 | 
            +
                # @return [String] the raw xml of the application response
         | 
| 6 12 | 
             
                attr_reader :xml
         | 
| 7 13 |  | 
| 8 14 | 
             
                validate :response_must_validate_against_schema
         | 
| 9 15 |  | 
| 16 | 
            +
                # Initializes the {ApplicationResponse} with an application response xml and bank
         | 
| 17 | 
            +
                #
         | 
| 18 | 
            +
                # @param app_resp [#to_s] the application response xml
         | 
| 19 | 
            +
                # @param bank [Symbol] the bank from which the application response came from
         | 
| 10 20 | 
             
                def initialize(app_resp, bank)
         | 
| 11 21 | 
             
                  @xml = app_resp
         | 
| 12 22 | 
             
                  @bank = bank
         | 
| 13 23 | 
             
                end
         | 
| 14 24 |  | 
| 25 | 
            +
                # The application response as a nokogiri xml document
         | 
| 26 | 
            +
                #
         | 
| 27 | 
            +
                # @return [Nokogiri::XML::Document] the application response as a nokogiri document
         | 
| 15 28 | 
             
                def doc
         | 
| 16 29 | 
             
                  @doc ||= xml_doc @xml
         | 
| 17 30 | 
             
                end
         | 
| 18 31 |  | 
| 19 | 
            -
                # Checks that the hash value reported in the signature matches the  | 
| 32 | 
            +
                # Checks that the hash value reported in the signature matches the one that is calculated
         | 
| 33 | 
            +
                # locally
         | 
| 34 | 
            +
                #
         | 
| 35 | 
            +
                # @return [true] if hashes match
         | 
| 36 | 
            +
                # @return [false] if hashes don't match
         | 
| 20 37 | 
             
                def hashes_match?
         | 
| 21 38 | 
             
                  are = doc.clone
         | 
| 22 39 |  | 
| @@ -31,19 +48,36 @@ module Sepa | |
| 31 48 | 
             
                  false
         | 
| 32 49 | 
             
                end
         | 
| 33 50 |  | 
| 34 | 
            -
                # Checks that the signature  | 
| 51 | 
            +
                # Checks that the signature has been calculated with the private key of the certificate's public
         | 
| 52 | 
            +
                # key.
         | 
| 53 | 
            +
                #
         | 
| 54 | 
            +
                # @return [true] if signature can be verified
         | 
| 55 | 
            +
                # @return [false] if signature fails to verify
         | 
| 35 56 | 
             
                def signature_is_valid?
         | 
| 36 57 | 
             
                  validate_signature(doc, certificate, :normal)
         | 
| 37 58 | 
             
                end
         | 
| 38 59 |  | 
| 60 | 
            +
                # Returns the raw xml of the application response
         | 
| 61 | 
            +
                #
         | 
| 62 | 
            +
                # @return [String] the raw xml of the application response
         | 
| 39 63 | 
             
                def to_s
         | 
| 40 64 | 
             
                  @xml
         | 
| 41 65 | 
             
                end
         | 
| 42 66 |  | 
| 67 | 
            +
                # The certificate which private key has been used to sign the application response
         | 
| 68 | 
            +
                #
         | 
| 69 | 
            +
                # @return [OpenSSL::X509::Certificate] if the certificate can be found
         | 
| 70 | 
            +
                # @return [nil] if the certificate cannot be found
         | 
| 71 | 
            +
                # @raise [OpenSSL::X509::CertificateError] if the certificate is not valid
         | 
| 43 72 | 
             
                def certificate
         | 
| 44 73 | 
             
                  extract_cert(doc, 'X509Certificate', DSIG)
         | 
| 45 74 | 
             
                end
         | 
| 46 75 |  | 
| 76 | 
            +
                # Checks whether the embedded certificate has been signed by the private key of the bank's root
         | 
| 77 | 
            +
                # certificate. The root certificate used varies by bank.
         | 
| 78 | 
            +
                #
         | 
| 79 | 
            +
                # @return [true] if the certificate is trusted
         | 
| 80 | 
            +
                # @return [false] if the certificate is not trusted
         | 
| 47 81 | 
             
                def certificate_is_trusted?
         | 
| 48 82 | 
             
                  root_certificate =
         | 
| 49 83 | 
             
                    case @bank
         | 
| @@ -58,6 +92,7 @@ module Sepa | |
| 58 92 |  | 
| 59 93 | 
             
                private
         | 
| 60 94 |  | 
| 95 | 
            +
                  # Validates that the response is valid against the application response schema
         | 
| 61 96 | 
             
                  def response_must_validate_against_schema
         | 
| 62 97 | 
             
                    check_validity_against_schema(doc, 'application_response.xsd')
         | 
| 63 98 | 
             
                  end
         | 
| @@ -1,7 +1,13 @@ | |
| 1 1 | 
             
            module Sepa
         | 
| 2 | 
            +
             | 
| 3 | 
            +
              # Contains functionality to check the attributes passed to {Client}. Uses
         | 
| 4 | 
            +
              # ActiveModel::Validations for the actual validation.
         | 
| 2 5 | 
             
              module AttributeChecks
         | 
| 3 6 | 
             
                include ErrorMessages
         | 
| 4 7 |  | 
| 8 | 
            +
                # Commands which are allowed for a specific bank
         | 
| 9 | 
            +
                #
         | 
| 10 | 
            +
                # @return [Array<Symbol>] the commands which are allowed for {Client#bank}.
         | 
| 5 11 | 
             
                def allowed_commands
         | 
| 6 12 | 
             
                  case bank
         | 
| 7 13 | 
             
                  when :nordea
         | 
| @@ -14,10 +20,12 @@ module Sepa | |
| 14 20 | 
             
                  end
         | 
| 15 21 | 
             
                end
         | 
| 16 22 |  | 
| 23 | 
            +
                # Checks that {Client#command} is included in {#allowed_commands}
         | 
| 17 24 | 
             
                def check_command
         | 
| 18 25 | 
             
                  errors.add(:command, "Invalid command") unless allowed_commands.include? command
         | 
| 19 26 | 
             
                end
         | 
| 20 27 |  | 
| 28 | 
            +
                # Checks that signing keys and certificates can be initialized properly.
         | 
| 21 29 | 
             
                def check_keys
         | 
| 22 30 | 
             
                  return if [:get_certificate, :get_bank_certificate, :create_certificate].include? command
         | 
| 23 31 |  | 
| @@ -34,6 +42,7 @@ module Sepa | |
| 34 42 | 
             
                  end
         | 
| 35 43 | 
             
                end
         | 
| 36 44 |  | 
| 45 | 
            +
                # Checks that signing certificate signing request can be initialized properly.
         | 
| 37 46 | 
             
                def check_signing_csr
         | 
| 38 47 | 
             
                  return unless [:get_certificate, :create_certificate].include? command
         | 
| 39 48 |  | 
| @@ -42,6 +51,7 @@ module Sepa | |
| 42 51 | 
             
                  end
         | 
| 43 52 | 
             
                end
         | 
| 44 53 |  | 
| 54 | 
            +
                # Checks that encryption certificate signing request can be initialized properly.
         | 
| 45 55 | 
             
                def check_encryption_cert_request
         | 
| 46 56 | 
             
                  return unless command == :create_certificate
         | 
| 47 57 |  | 
| @@ -50,6 +60,7 @@ module Sepa | |
| 50 60 | 
             
                  end
         | 
| 51 61 | 
             
                end
         | 
| 52 62 |  | 
| 63 | 
            +
                # Checks that {Client#file_type} is proper
         | 
| 53 64 | 
             
                def check_file_type
         | 
| 54 65 | 
             
                  return unless [:upload_file, :download_file_list, :download_file].include? command
         | 
| 55 66 |  | 
| @@ -58,6 +69,7 @@ module Sepa | |
| 58 69 | 
             
                  end
         | 
| 59 70 | 
             
                end
         | 
| 60 71 |  | 
| 72 | 
            +
                # Checks that {Client#target_id} is valid.
         | 
| 61 73 | 
             
                def check_target_id
         | 
| 62 74 | 
             
                  return if [:get_user_info,
         | 
| 63 75 | 
             
                             :get_certificate,
         | 
| @@ -70,6 +82,11 @@ module Sepa | |
| 70 82 | 
             
                  check_presence_and_length(:target_id, 80, TARGET_ID_ERROR_MESSAGE)
         | 
| 71 83 | 
             
                end
         | 
| 72 84 |  | 
| 85 | 
            +
                # Checks presence and length of an attribute
         | 
| 86 | 
            +
                #
         | 
| 87 | 
            +
                # @param attribute [Symbol] the attribute to validate
         | 
| 88 | 
            +
                # @param length [Integer] the maximum length of the attribute
         | 
| 89 | 
            +
                # @param error_message [#to_s] the error message to display if the validation fails
         | 
| 73 90 | 
             
                def check_presence_and_length(attribute, length, error_message)
         | 
| 74 91 | 
             
                  check = true
         | 
| 75 92 | 
             
                  check &&= send(attribute)
         | 
| @@ -80,6 +97,8 @@ module Sepa | |
| 80 97 | 
             
                  errors.add(attribute, error_message) unless check
         | 
| 81 98 | 
             
                end
         | 
| 82 99 |  | 
| 100 | 
            +
                # Checks that the content (payload) of the request is somewhat correct. This validation is only
         | 
| 101 | 
            +
                # run when {Client#command} is `:upload_file`.
         | 
| 83 102 | 
             
                def check_content
         | 
| 84 103 | 
             
                  return unless command == :upload_file
         | 
| 85 104 |  | 
| @@ -91,12 +110,15 @@ module Sepa | |
| 91 110 | 
             
                  errors.add(:content, CONTENT_ERROR_MESSAGE) unless check
         | 
| 92 111 | 
             
                end
         | 
| 93 112 |  | 
| 113 | 
            +
                # Checks that the {Client#pin} used in certificate requests in valid
         | 
| 94 114 | 
             
                def check_pin
         | 
| 95 115 | 
             
                  return unless [:create_certificate, :get_certificate].include? command
         | 
| 96 116 |  | 
| 97 117 | 
             
                  check_presence_and_length(:pin, 20, PIN_ERROR_MESSAGE)
         | 
| 98 118 | 
             
                end
         | 
| 99 119 |  | 
| 120 | 
            +
                # Checks that {Client#environment} is included in {Client::ENVIRONMENTS}. Not run if
         | 
| 121 | 
            +
                # {Client#command} is `:get_bank_certificate`.
         | 
| 100 122 | 
             
                def check_environment
         | 
| 101 123 | 
             
                  return if command == :get_bank_certificate
         | 
| 102 124 |  | 
| @@ -105,12 +127,15 @@ module Sepa | |
| 105 127 | 
             
                  end
         | 
| 106 128 | 
             
                end
         | 
| 107 129 |  | 
| 130 | 
            +
                # Checks that {Client#customer_id} is valid
         | 
| 108 131 | 
             
                def check_customer_id
         | 
| 109 132 | 
             
                  unless customer_id && customer_id.respond_to?(:length) && customer_id.length.between?(1, 16)
         | 
| 110 133 | 
             
                    errors.add(:customer_id, CUSTOMER_ID_ERROR_MESSAGE)
         | 
| 111 134 | 
             
                  end
         | 
| 112 135 | 
             
                end
         | 
| 113 136 |  | 
| 137 | 
            +
                # Checks that {Client#bank_encryption_certificate} can be initialized properly. Only run if
         | 
| 138 | 
            +
                # {Client#bank} is `:danske` and {Client#command} is not `:get_bank_certificate`.
         | 
| 114 139 | 
             
                def check_encryption_certificate
         | 
| 115 140 | 
             
                  return unless bank == :danske
         | 
| 116 141 | 
             
                  return if command == :get_bank_certificate
         | 
| @@ -125,6 +150,7 @@ module Sepa | |
| 125 150 | 
             
                  errors.add(:bank_encryption_certificate, ENCRYPTION_CERT_ERROR_MESSAGE)
         | 
| 126 151 | 
             
                end
         | 
| 127 152 |  | 
| 153 | 
            +
                # Checks that {Client#status} is included in {Client::STATUSES}.
         | 
| 128 154 | 
             
                def check_status
         | 
| 129 155 | 
             
                  return unless [:download_file_list, :download_file].include? command
         | 
| 130 156 |  | 
| @@ -133,12 +159,16 @@ module Sepa | |
| 133 159 | 
             
                  end
         | 
| 134 160 | 
             
                end
         | 
| 135 161 |  | 
| 162 | 
            +
                # Checks presence and length of {Client#file_reference} if {Client#command} is `:download_file`
         | 
| 136 163 | 
             
                def check_file_reference
         | 
| 137 164 | 
             
                  return unless command == :download_file
         | 
| 138 165 |  | 
| 139 166 | 
             
                  check_presence_and_length :file_reference, 33, FILE_REFERENCE_ERROR_MESSAGE
         | 
| 140 167 | 
             
                end
         | 
| 141 168 |  | 
| 169 | 
            +
                # Checks that {Client#encryption_private_key} can be initialized properly. Is only run if
         | 
| 170 | 
            +
                # {Client#bank} is `:danske` and {Client#command} is not `:create_certificate` or
         | 
| 171 | 
            +
                # `:get_bank_certificate`.
         | 
| 142 172 | 
             
                def check_encryption_private_key
         | 
| 143 173 | 
             
                  return unless bank == :danske
         | 
| 144 174 | 
             
                  return if [:create_certificate, :get_bank_certificate].include? command
         | 
| @@ -1,49 +1,90 @@ | |
| 1 1 | 
             
            module Sepa
         | 
| 2 | 
            +
             | 
| 3 | 
            +
              # Handles Danske Bank specific {Response} functionality. Mainly decryption and certificate
         | 
| 4 | 
            +
              # specific stuff.
         | 
| 2 5 | 
             
              class DanskeResponse < Response
         | 
| 3 6 |  | 
| 4 7 | 
             
                validate :valid_get_bank_certificate_response
         | 
| 5 8 | 
             
                validate :can_be_decrypted_with_given_key
         | 
| 6 9 |  | 
| 10 | 
            +
                # @return [String]
         | 
| 11 | 
            +
                # @see Response#application_response
         | 
| 7 12 | 
             
                def application_response
         | 
| 8 13 | 
             
                  @application_response ||= decrypt_application_response
         | 
| 9 14 | 
             
                end
         | 
| 10 15 |  | 
| 16 | 
            +
                # Returns the bank's encryption certificate which is used to encrypt messages sent to the bank.
         | 
| 17 | 
            +
                # The certificate is only present in `:get_bank_certificate` responess.
         | 
| 18 | 
            +
                #
         | 
| 19 | 
            +
                # @return [OpenSSL::X509::Certificate] if {#command} is `:get_bank_certificate`
         | 
| 20 | 
            +
                # @return [nil] if command is any other
         | 
| 11 21 | 
             
                def bank_encryption_certificate
         | 
| 12 22 | 
             
                  return unless @command == :get_bank_certificate
         | 
| 13 23 |  | 
| 14 24 | 
             
                  @bank_encryption_certificate ||= extract_cert(doc, 'BankEncryptionCert', DANSKE_PKI)
         | 
| 15 25 | 
             
                end
         | 
| 16 26 |  | 
| 27 | 
            +
                # Returns the bank's signing certificate which is used by the bank to sign the responses. The
         | 
| 28 | 
            +
                # certificate is only present in `:get_bank_certificate` responses
         | 
| 29 | 
            +
                #
         | 
| 30 | 
            +
                # @return [OpenSSL::X509::Certificate] if {#command} is `:get_bank_certificate`
         | 
| 31 | 
            +
                # @return [nil] if {#command} is any other
         | 
| 17 32 | 
             
                def bank_signing_certificate
         | 
| 18 33 | 
             
                  return unless @command == :get_bank_certificate
         | 
| 19 34 |  | 
| 20 35 | 
             
                  @bank_signing_certificate ||= extract_cert(doc, 'BankSigningCert', DANSKE_PKI)
         | 
| 21 36 | 
             
                end
         | 
| 22 37 |  | 
| 38 | 
            +
                # Returns the bank's root certificate which is the certificate that is used to sign bank's other
         | 
| 39 | 
            +
                # certificates. Only present in `:get_bank_certificate` responses.
         | 
| 40 | 
            +
                #
         | 
| 41 | 
            +
                # @return [OpenSSL::X509::Certificate] if {#command} is `:get_bank_certificate`
         | 
| 42 | 
            +
                # @return [nil] if {#command} is any other
         | 
| 23 43 | 
             
                def bank_root_certificate
         | 
| 24 44 | 
             
                  return unless @command == :get_bank_certificate
         | 
| 25 45 |  | 
| 26 46 | 
             
                  @bank_root_certificate ||= extract_cert(doc, 'BankRootCert', DANSKE_PKI)
         | 
| 27 47 | 
             
                end
         | 
| 28 48 |  | 
| 49 | 
            +
                # Returns own encryption certificate which has been signed by the bank. Only present in
         | 
| 50 | 
            +
                # `:create_certificate` responses
         | 
| 51 | 
            +
                #
         | 
| 52 | 
            +
                # @return [OpenSSL::X509::Certificate] if {#command} is `:create_certificate`
         | 
| 53 | 
            +
                # @return [nil] if command is any other
         | 
| 29 54 | 
             
                def own_encryption_certificate
         | 
| 30 55 | 
             
                  return unless @command == :create_certificate
         | 
| 31 56 |  | 
| 32 57 | 
             
                  @own_encryption_certificate ||= extract_cert(doc, 'EncryptionCert', DANSKE_PKI)
         | 
| 33 58 | 
             
                end
         | 
| 34 59 |  | 
| 60 | 
            +
                # Returns own signing certificate which has been signed by the bank. Is used to sign requests
         | 
| 61 | 
            +
                # sent to the bank. Is only present in `:create_certificate` responses.
         | 
| 62 | 
            +
                #
         | 
| 63 | 
            +
                # @return [OpenSSL::X509::Certificate] if {#command} is `:create_certificate`
         | 
| 64 | 
            +
                # @return [nil] if command is any other
         | 
| 35 65 | 
             
                def own_signing_certificate
         | 
| 36 66 | 
             
                  return unless @command == :create_certificate
         | 
| 37 67 |  | 
| 38 68 | 
             
                  @own_signing_certificate ||= extract_cert(doc, 'SigningCert', DANSKE_PKI)
         | 
| 39 69 | 
             
                end
         | 
| 40 70 |  | 
| 71 | 
            +
                # Returns the CA certificate that has been used to sign own signing and encryption certificates.
         | 
| 72 | 
            +
                # Only present in `:create_certificate` responses
         | 
| 73 | 
            +
                #
         | 
| 74 | 
            +
                # @return [OpenSSL::X509::Certificate] if {#command} is `:create_certificate`
         | 
| 75 | 
            +
                # @return [nil] if command is any other
         | 
| 41 76 | 
             
                def ca_certificate
         | 
| 42 77 | 
             
                  return unless @command == :create_certificate
         | 
| 43 78 |  | 
| 44 79 | 
             
                  @ca_certificate ||= extract_cert(doc, 'CACert', DANSKE_PKI)
         | 
| 45 80 | 
             
                end
         | 
| 46 81 |  | 
| 82 | 
            +
                # Extract certificate that has been used to sign the response. This overrides
         | 
| 83 | 
            +
                # {Response#certificate} method with specific functionality for `:get_bank_certificate` and
         | 
| 84 | 
            +
                # `:create_certificate` commands. Otherwise just calls {Response#certificate}
         | 
| 85 | 
            +
                #
         | 
| 86 | 
            +
                # @return [OpenSSL::X509::Certificate]
         | 
| 87 | 
            +
                # @raise [OpenSSL::X509::CertificateError] if certificate cannot be processed
         | 
| 47 88 | 
             
                def certificate
         | 
| 48 89 | 
             
                  if [:get_bank_certificate, :create_certificate].include? @command
         | 
| 49 90 | 
             
                    @certificate ||= begin
         | 
| @@ -54,6 +95,13 @@ module Sepa | |
| 54 95 | 
             
                  end
         | 
| 55 96 | 
             
                end
         | 
| 56 97 |  | 
| 98 | 
            +
                # Extract response code from the response. Overrides super method when {#command} is
         | 
| 99 | 
            +
                # `:get_bank_certificate` or `:create_certificate` because response code node is named
         | 
| 100 | 
            +
                # differently in those responses.
         | 
| 101 | 
            +
                #
         | 
| 102 | 
            +
                # @return [String] if response code is found
         | 
| 103 | 
            +
                # @return [nil] if response code cannot be found
         | 
| 104 | 
            +
                # @see Response#response_code
         | 
| 57 105 | 
             
                def response_code
         | 
| 58 106 | 
             
                  return super unless [:get_bank_certificate, :create_certificate].include? @command
         | 
| 59 107 |  | 
| @@ -61,6 +109,12 @@ module Sepa | |
| 61 109 | 
             
                  node.content if node
         | 
| 62 110 | 
             
                end
         | 
| 63 111 |  | 
| 112 | 
            +
                # Checks whether certificate embedded in the response has been signed with the bank's root
         | 
| 113 | 
            +
                # certificate. Always returns true when {#command} is `:get_bank_certificate`, because the
         | 
| 114 | 
            +
                # certificate is not present with that command.
         | 
| 115 | 
            +
                #
         | 
| 116 | 
            +
                # @return [true] if certificate is trusted
         | 
| 117 | 
            +
                # @return [false] if certificate is not trusted
         | 
| 64 118 | 
             
                def certificate_is_trusted?
         | 
| 65 119 | 
             
                  return true if @command == :get_bank_certificate
         | 
| 66 120 |  | 
| @@ -69,6 +123,14 @@ module Sepa | |
| 69 123 |  | 
| 70 124 | 
             
                private
         | 
| 71 125 |  | 
| 126 | 
            +
                  # Finds a node by its reference URI from Danske Bank's certificate responses. If {#command} is
         | 
| 127 | 
            +
                  # other than `:get_bank_certificate` or `:create_certificate` returns super. This method is
         | 
| 128 | 
            +
                  # needed because Danske Bank uses a different way to reference nodes in their certificate
         | 
| 129 | 
            +
                  # responses.
         | 
| 130 | 
            +
                  #
         | 
| 131 | 
            +
                  # @param uri [String] reference URI of the node to find
         | 
| 132 | 
            +
                  # @return [Nokogiri::XML::Node] node with signature removed from its document since signature
         | 
| 133 | 
            +
                  #   has to be removed for canonicalization and hash calculation
         | 
| 72 134 | 
             
                  def find_node_by_uri(uri)
         | 
| 73 135 | 
             
                    return super unless [:get_bank_certificate, :create_certificate].include? @command
         | 
| 74 136 |  | 
| @@ -77,6 +139,15 @@ module Sepa | |
| 77 139 | 
             
                    doc_without_signature.at("[xml|id='#{uri}']")
         | 
| 78 140 | 
             
                  end
         | 
| 79 141 |  | 
| 142 | 
            +
                  # Decrypts the application response in the response. Starts by calling {#decrypt_embedded_key}
         | 
| 143 | 
            +
                  # method to get the key used in encrypting the application response. After this the encrypted
         | 
| 144 | 
            +
                  # data is retrieved from the document and base64 decoded. After this the iv
         | 
| 145 | 
            +
                  # (initialization vector) is extracted from the encrypted data and a decipher with the
         | 
| 146 | 
            +
                  # 'DES-EDE3-CBC' algorithm is initialized (This is used by banks as encryption algorithm) and
         | 
| 147 | 
            +
                  # its key and iv set accordingly and mode changes to decrypt. After this the data is decrypted
         | 
| 148 | 
            +
                  # and returned as string.
         | 
| 149 | 
            +
                  #
         | 
| 150 | 
            +
                  # @return [String] the decrypted application response as raw xml
         | 
| 80 151 | 
             
                  def decrypt_application_response
         | 
| 81 152 | 
             
                    key = decrypt_embedded_key
         | 
| 82 153 |  | 
| @@ -96,6 +167,8 @@ module Sepa | |
| 96 167 | 
             
                    decipher.update(encypted_data) + decipher.final
         | 
| 97 168 | 
             
                  end
         | 
| 98 169 |  | 
| 170 | 
            +
                  # Validates get bank certificate response. Response is valid if service fault is not returned
         | 
| 171 | 
            +
                  # from the bank.
         | 
| 99 172 | 
             
                  def valid_get_bank_certificate_response
         | 
| 100 173 | 
             
                    return unless @command == :get_bank_certificate
         | 
| 101 174 |  | 
| @@ -104,6 +177,11 @@ module Sepa | |
| 104 177 | 
             
                    end
         | 
| 105 178 | 
             
                  end
         | 
| 106 179 |  | 
| 180 | 
            +
                  # Extracts the encrypted application response from the response and returns it as a nokogiri
         | 
| 181 | 
            +
                  # document
         | 
| 182 | 
            +
                  #
         | 
| 183 | 
            +
                  # @return [Nokogiri::XML] the encrypted application response if it is found
         | 
| 184 | 
            +
                  # @return [nil] if the application response cannot be found
         | 
| 107 185 | 
             
                  def encrypted_application_response
         | 
| 108 186 | 
             
                    @encrypted_application_response ||= begin
         | 
| 109 187 | 
             
                      encrypted_application_response = extract_application_response(BXD)
         | 
| @@ -111,6 +189,8 @@ module Sepa | |
| 111 189 | 
             
                    end
         | 
| 112 190 | 
             
                  end
         | 
| 113 191 |  | 
| 192 | 
            +
                  # Validates that the encrypted key in the response can be decrypted with the private key given
         | 
| 193 | 
            +
                  # to the response in the parameters. Response is invalid if this cannot be done.
         | 
| 114 194 | 
             
                  def can_be_decrypted_with_given_key
         | 
| 115 195 | 
             
                    return if [:get_bank_certificate, :create_certificate].include? @command
         | 
| 116 196 | 
             
                    return unless encrypted_application_response.css('CipherValue', 'xmlns' => XMLENC)[0]
         | 
| @@ -120,6 +200,12 @@ module Sepa | |
| 120 200 | 
             
                    end
         | 
| 121 201 | 
             
                  end
         | 
| 122 202 |  | 
| 203 | 
            +
                  # Decrypts (assymetrically) the symmetric encryption key embedded in the response with the
         | 
| 204 | 
            +
                  # private key given to the response in the parameters. The key is later used to decrypt the
         | 
| 205 | 
            +
                  # application response.
         | 
| 206 | 
            +
                  #
         | 
| 207 | 
            +
                  # @return [String] the encryption key as a string
         | 
| 208 | 
            +
                  # @return [nil] if the key cannot be decrypted with the given key
         | 
| 123 209 | 
             
                  def decrypt_embedded_key
         | 
| 124 210 | 
             
                    enc_key = encrypted_application_response.css('CipherValue', 'xmlns' => XMLENC)[0].content
         | 
| 125 211 | 
             
                    enc_key = decode enc_key
         |