Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

H003 schematic support #154

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
EPICS is a ruby implementation of the [EBICS](https://www.ebics.org/) (Electronic Banking Internet
Communication Standard).

It supports EBICS 2.5.
It supports EBICS 2.4, 2.5 and 3.0.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the readme could reflect which one is the default when initializing a new or existing client and how to work with another version


The client supports the complete initialization process comprising INI, HIA and HPB including the
INI letter generation. It offers support for the most common download and upload order types
Expand Down
6 changes: 5 additions & 1 deletion lib/epics.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
require 'securerandom'
require 'time'
require "epics/version"
require "epics/key"
require "epics/signature_algorithm"
require "epics/signature_algorithm/base"
require "epics/signature_algorithm/rsa"
require "epics/signature_algorithm/rsapss"
require "epics/signature_algorithm/rsapkcs1"
require "epics/response"
require "epics/error"
require 'epics/letter_renderer'
Expand Down
118 changes: 88 additions & 30 deletions lib/epics/client.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
class Epics::Client
extend Forwardable

attr_accessor :passphrase, :url, :host_id, :user_id, :partner_id, :keys, :keys_content
attr_accessor :passphrase, :url, :host_id, :user_id, :partner_id, :keys, :keys_content, :current_order_id
attr_reader :version
attr_accessor :signature_version
attr_writer :iban, :bic, :name
attr_accessor :locale

def_delegators :connection, :post

VERSION_H3 = 'H003'
VERSION_H4 = 'H004'
VERSION_H5 = 'H005'
VERSION_A5 = 'A005'
VERSION_A6 = 'A006'

VERSIONS = [VERSION_H3, VERSION_H4, VERSION_H5]

USER_AGENT = "EPICS v#{Epics::VERSION}"

def initialize(keys_content, passphrase, url, host_id, user_id, partner_id)
self.keys_content = keys_content.respond_to?(:read) ? keys_content.read : keys_content if keys_content
self.passphrase = passphrase
Expand All @@ -16,6 +28,26 @@ def initialize(keys_content, passphrase, url, host_id, user_id, partner_id)
self.user_id = user_id
self.partner_id = partner_id
self.locale = :de
self.current_order_id = 0
self.version = VERSION_H4
self.signature_version = VERSION_A6

yield self if block_given?
end

def version=(version)
raise ArgumentError, "Unsupported version: #{version}" unless VERSIONS.include?(version)

@version = version
end

def urn_schema
case version
when VERSION_H3
"http://www.ebics.org/#{version}"
when VERSION_H4, VERSION_H5
"urn:org:ebics:#{version}"
end
end

def inspect
Expand All @@ -25,24 +57,37 @@ def inspect
@partner_id=\"#{self.partner_id}\""
end

def e
keys["E002"]
def next_order_id
raise 'Order ID overflow' if current_order_id >= 1679615
self.current_order_id += 1
end

def encryption_version
'E002'
end

def encryption_key
keys[encryption_version]
end

def signature_key
keys[signature_version]
end

def a
keys["A006"]
def authentication_version
'X002'
end

def x
keys["X002"]
def authentication_key
keys[authentication_version]
end

def bank_e
keys["#{host_id.upcase}.E002"]
def bank_encryption_key
keys["#{host_id.upcase}.#{encryption_version}"]
end

def bank_x
keys["#{host_id.upcase}.X002"]
def bank_authentication_key
keys["#{host_id.upcase}.#{authentication_version}"]
end

def name
Expand All @@ -61,10 +106,15 @@ def order_types
@order_types ||= (self.HTD; @order_types)
end

def self.setup(passphrase, url, host_id, user_id, partner_id, keysize = 2048)
client = new(nil, passphrase, url, host_id, user_id, partner_id)
client.keys = %w(A006 X002 E002).each_with_object({}) do |type, memo|
memo[type] = Epics::Key.new( OpenSSL::PKey::RSA.generate(keysize) )
def self.setup(passphrase, url, host_id, user_id, partner_id, keysize = 2048, &block)
client = new(nil, passphrase, url, host_id, user_id, partner_id, &block)
client.keys = [client.signature_version, client.authentication_version, client.encryption_version].each_with_object({}) do |type, memo|
memo[type] = case type
when VERSION_A6
Epics::SignatureAlgorithm::RsaPss.new( OpenSSL::PKey::RSA.generate(keysize) )
else
Epics::SignatureAlgorithm::RsaPkcs1.new( OpenSSL::PKey::RSA.generate(keysize) )
end
end

client
Expand Down Expand Up @@ -108,7 +158,7 @@ def INI
end

def HPB
Nokogiri::XML(download(Epics::HPB)).xpath("//xmlns:PubKeyValue", xmlns: "urn:org:ebics:H004").each do |node|
Nokogiri::XML(download(Epics::HPB)).xpath("//xmlns:PubKeyValue", xmlns: urn_schema).each do |node|
type = node.parent.last_element_child.content

modulus = Base64.decode64(node.at_xpath(".//*[local-name() = 'Modulus']").content)
Expand All @@ -120,10 +170,15 @@ def HPB

bank = OpenSSL::PKey::RSA.new(OpenSSL::ASN1::Sequence(sequence).to_der)

self.keys["#{host_id.upcase}.#{type}"] = Epics::Key.new(bank)
self.keys["#{host_id.upcase}.#{type}"] = case type
when VERSION_A6
Epics::SignatureAlgorithm::RsaPss.new(bank)
else
Epics::SignatureAlgorithm::RsaPkcs1.new(bank)
end
end

[bank_x, bank_e]
[bank_authentication_key, bank_encryption_key]
end

def AZV(document)
Expand Down Expand Up @@ -215,15 +270,15 @@ def Z54(from, to)
end

def HAA
Nokogiri::XML(download(Epics::HAA)).at_xpath("//xmlns:OrderTypes", xmlns: "urn:org:ebics:H004").content.split(/\s/)
Nokogiri::XML(download(Epics::HAA)).at_xpath("//xmlns:OrderTypes", xmlns: urn_schema).content.split(/\s/)
end

def HTD
Nokogiri::XML(download(Epics::HTD)).tap do |htd|
@iban ||= htd.at_xpath("//xmlns:AccountNumber[@international='true']", xmlns: "urn:org:ebics:H004").text rescue nil
@bic ||= htd.at_xpath("//xmlns:BankCode[@international='true']", xmlns: "urn:org:ebics:H004").text rescue nil
@name ||= htd.at_xpath("//xmlns:Name", xmlns: "urn:org:ebics:H004").text rescue nil
@order_types ||= htd.search("//xmlns:OrderTypes", xmlns: "urn:org:ebics:H004").map{|o| o.content.split(/\s/) }.delete_if{|o| o == ""}.flatten
@iban ||= htd.at_xpath("//xmlns:AccountNumber[@international='true']", xmlns: urn_schema).text rescue nil
@bic ||= htd.at_xpath("//xmlns:BankCode[@international='true']", xmlns: urn_schema).text rescue nil
@name ||= htd.at_xpath("//xmlns:Name", xmlns: urn_schema).text rescue nil
@order_types ||= htd.search("//xmlns:OrderTypes", xmlns: urn_schema).map{|o| o.content.split(/\s/) }.delete_if{|o| o == ""}.flatten
end.to_xml
end

Expand Down Expand Up @@ -251,14 +306,12 @@ def save_keys(path)

def upload(order_type, document)
order = order_type.new(self, document)
res = post(url, order.to_xml).body
order.transaction_id = res.transaction_id

order_id = res.order_id
session = post(url, order.to_xml).body
order.transaction_id = session.transaction_id

res = post(url, order.to_transfer_xml).body

return res.transaction_id, [res.order_id, order_id].detect { |id| id.to_s.chars.any? }
return res.transaction_id, [res.order_id, session.order_id].detect { |id| id.to_s.chars.any? }
end

def download(order_type, *args, **options)
Expand All @@ -282,7 +335,7 @@ def download_and_unzip(order_type, *args, **options)
end

def connection
@connection ||= Faraday.new(headers: { 'Content-Type' => 'text/xml', user_agent: "EPICS v#{Epics::VERSION}"}, ssl: { verify: verify_ssl? }) do |faraday|
@connection ||= Faraday.new(headers: { 'Content-Type' => 'text/xml', user_agent: USER_AGENT }, ssl: { verify: verify_ssl? }) do |faraday|
faraday.use Epics::XMLSIG, { client: self }
faraday.use Epics::ParseEbics, { client: self}
# faraday.use MyAdapter
Expand All @@ -292,7 +345,12 @@ def connection

def extract_keys
JSON.load(self.keys_content).each_with_object({}) do |(type, key), memo|
memo[type] = Epics::Key.new(decrypt(key)) if key
memo[type] = case type
when VERSION_A6
Epics::SignatureAlgorithm::RsaPss.new(decrypt(key))
else
Epics::SignatureAlgorithm::RsaPkcs1.new(decrypt(key))
end if key
end
end

Expand Down
6 changes: 3 additions & 3 deletions lib/epics/generic_request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def auth_signature

def to_transfer_xml
Nokogiri::XML::Builder.new do |xml|
xml.send(root, 'xmlns:ds' => 'http://www.w3.org/2000/09/xmldsig#', 'xmlns' => 'urn:org:ebics:H004', 'Version' => 'H004', 'Revision' => '1') {
xml.send(root, 'xmlns:ds' => 'http://www.w3.org/2000/09/xmldsig#', 'xmlns' => client.urn_schema, 'Version' => client.version, 'Revision' => '1') {
xml.header(authenticate: true) {
xml.static {
xml.HostID host_id
Expand All @@ -76,7 +76,7 @@ def to_transfer_xml

def to_receipt_xml
Nokogiri::XML::Builder.new do |xml|
xml.send(root, 'xmlns:ds' => 'http://www.w3.org/2000/09/xmldsig#', 'xmlns' => 'urn:org:ebics:H004', 'Version' => 'H004', 'Revision' => '1') {
xml.send(root, 'xmlns:ds' => 'http://www.w3.org/2000/09/xmldsig#', 'xmlns' => client.urn_schema, 'Version' => client.version, 'Revision' => '1') {
xml.header(authenticate: true) {
xml.static {
xml.HostID host_id
Expand All @@ -98,7 +98,7 @@ def to_receipt_xml

def to_xml
Nokogiri::XML::Builder.new do |xml|
xml.send(root, 'xmlns:ds' => 'http://www.w3.org/2000/09/xmldsig#', 'xmlns' => 'urn:org:ebics:H004', 'Version' => 'H004', 'Revision'=> '1') {
xml.send(root, 'xmlns:ds' => 'http://www.w3.org/2000/09/xmldsig#', 'xmlns' => client.urn_schema, 'Version' => client.version, 'Revision'=> '1') {
xml.parent.add_child(header)
xml.parent.add_child(auth_signature)
xml.parent.add_child(body)
Expand Down
14 changes: 6 additions & 8 deletions lib/epics/generic_upload_request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,13 @@ def cipher
@cipher ||= OpenSSL::Cipher.new("aes-128-cbc").tap { |cipher| cipher.encrypt }
end

def digester
@digester ||= OpenSSL::Digest::SHA256.new
end

def body
Nokogiri::XML::Builder.new do |xml|
xml.body {
xml.DataTransfer {
xml.DataEncryptionInfo(authenticate: true) {
xml.EncryptionPubKeyDigest(client.bank_e.public_digest, Version: 'E002', Algorithm: "http://www.w3.org/2001/04/xmlenc#sha256")
xml.TransactionKey Base64.encode64(client.bank_e.key.public_encrypt(self.key)).gsub(/\n/,'')
xml.EncryptionPubKeyDigest(client.bank_encryption_key.public_digest, Version: client.encryption_version, Algorithm: "http://www.w3.org/2001/04/xmlenc#sha256")
xml.TransactionKey Base64.encode64(client.bank_encryption_key.key.public_encrypt(self.key)).gsub(/\n/,'')
}
xml.SignatureData(encrypted_order_signature, authenticate: true)
}
Expand All @@ -36,7 +32,7 @@ def order_signature
Nokogiri::XML::Builder.new do |xml|
xml.UserSignatureData('xmlns' => 'http://www.ebics.org/S001', 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xsi:schemaLocation' => 'http://www.ebics.org/S001 http://www.ebics.org/S001/ebics_signature.xsd') {
xml.OrderSignatureData {
xml.SignatureVersion "A006"
xml.SignatureVersion client.signature_version
xml.SignatureValue signature_value
xml.PartnerID partner_id
xml.UserID user_id
Expand All @@ -46,7 +42,9 @@ def order_signature
end

def signature_value
client.a.sign( digester.digest(document.gsub(/\n|\r/, "")) )
Base64.encode64(
client.signature_key.sign(client.signature_key.digester.digest(document.gsub(/\n|\r/, "")))
).gsub("\n", '')
end

def encrypt(d)
Expand Down
12 changes: 9 additions & 3 deletions lib/epics/header_request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def initialize(client)

def build(options = {})
options[:with_bank_pubkey_digests] = true if options[:with_bank_pubkey_digests].nil?
options[:security_medium] = 0 if options[:security_medium].nil?

Nokogiri::XML::Builder.new do |xml|
xml.header(authenticate: true) {
Expand All @@ -25,16 +26,17 @@ def build(options = {})
xml.Product(PRODUCT_NAME, 'Language' => PRODUCT_LANG)
xml.OrderDetails {
xml.OrderType options[:order_type]
xml.OrderID b36encode(client.next_order_id) if client.version == Epics::Client::VERSION_H3
xml.OrderAttribute options[:order_attribute]
xml.StandardOrderParams {
build_attributes(xml, options[:order_params])
} if options[:order_params]
}
xml.BankPubKeyDigests {
xml.Authentication(client.bank_x.public_digest, Version: 'X002', Algorithm: 'http://www.w3.org/2001/04/xmlenc#sha256')
xml.Encryption(client.bank_e.public_digest, Version: 'E002', Algorithm: 'http://www.w3.org/2001/04/xmlenc#sha256')
xml.Authentication(client.bank_authentication_key.public_digest, Version: client.authentication_version, Algorithm: 'http://www.w3.org/2001/04/xmlenc#sha256')
xml.Encryption(client.bank_encryption_key.public_digest, Version: client.encryption_version, Algorithm: 'http://www.w3.org/2001/04/xmlenc#sha256')
} if options[:with_bank_pubkey_digests]
xml.SecurityMedium '0000'
xml.SecurityMedium b36encode(options[:security_medium]) if options[:security_medium]
xml.NumSegments options[:num_segments] if options[:num_segments]
}
xml.mutable {
Expand All @@ -57,4 +59,8 @@ def build_attributes(xml, attributes)
end
end
end

def b36encode(number)
tobischo marked this conversation as resolved.
Show resolved Hide resolved
number.to_s(36).upcase.rjust(4, '0')
end
end
16 changes: 8 additions & 8 deletions lib/epics/hia.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,24 @@ def body

def order_data
Nokogiri::XML::Builder.new do |xml|
xml.HIARequestOrderData('xmlns:ds' => 'http://www.w3.org/2000/09/xmldsig#', 'xmlns' => 'urn:org:ebics:H004') {
xml.HIARequestOrderData('xmlns:ds' => 'http://www.w3.org/2000/09/xmldsig#', 'xmlns' => client.urn_schema) {
xml.AuthenticationPubKeyInfo {
xml.PubKeyValue {
xml.send('ds:RSAKeyValue') {
xml.send('ds:Modulus', Base64.strict_encode64([client.x.n].pack("H*")))
xml.send('ds:Exponent', Base64.strict_encode64(client.x.key.e.to_s(2)))
xml.send('ds:Modulus', Base64.strict_encode64([client.authentication_key.n].pack("H*")))
xml.send('ds:Exponent', Base64.strict_encode64(client.authentication_key.key.e.to_s(2)))
}
}
xml.AuthenticationVersion 'X002'
xml.AuthenticationVersion client.authentication_version
}
xml.EncryptionPubKeyInfo{
xml.PubKeyValue {
xml.send('ds:RSAKeyValue') {
xml.send('ds:Modulus', Base64.strict_encode64([client.e.n].pack("H*")))
xml.send('ds:Exponent', Base64.strict_encode64(client.e.key.e.to_s(2)))
xml.send('ds:Modulus', Base64.strict_encode64([client.encryption_key.n].pack("H*")))
xml.send('ds:Exponent', Base64.strict_encode64(client.encryption_key.key.e.to_s(2)))
}
}
xml.EncryptionVersion 'E002'
xml.EncryptionVersion client.encryption_version
}
xml.PartnerID partner_id
xml.UserID user_id
Expand All @@ -51,7 +51,7 @@ def order_data

def to_xml
Nokogiri::XML::Builder.new do |xml|
xml.send(root, 'xmlns:ds' => 'http://www.w3.org/2000/09/xmldsig#', 'xmlns' => 'urn:org:ebics:H004', 'Version' => 'H004', 'Revision' => '1') {
xml.send(root, 'xmlns:ds' => 'http://www.w3.org/2000/09/xmldsig#', 'xmlns' => client.urn_schema, 'Version' => client.version, 'Revision' => '1') {
xml.parent.add_child(header)
xml.parent.add_child(body)
}
Expand Down
Loading