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 11 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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
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 default setting is 2.5.

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
8 changes: 7 additions & 1 deletion lib/epics.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@
require 'securerandom'
require 'time'
require "epics/version"
require "epics/key"
require "epics/keyring"
require "epics/signature"
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
199 changes: 159 additions & 40 deletions lib/epics/client.rb
Original file line number Diff line number Diff line change
@@ -1,48 +1,105 @@
class Epics::Client
extend Forwardable

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

def_delegators :connection, :post

def initialize(keys_content, passphrase, url, host_id, user_id, partner_id, locale: Epics::DEFAULT_LOCALE, product_name: Epics::DEFAULT_PRODUCT_NAME)
self.keys_content = keys_content.respond_to?(:read) ? keys_content.read : keys_content if keys_content
self.passphrase = passphrase
self.keys = extract_keys if keys_content
USER_AGENT = "EPICS v#{Epics::VERSION}"

def initialize(keys_content, passphrase, url, host_id, user_id, partner_id, options = {})
self.url = url
self.host_id = host_id
self.user_id = user_id
self.partner_id = partner_id
self.locale = locale
self.product_name = product_name
self.locale = options[:locale] || Epics::DEFAULT_LOCALE
self.product_name = options[:product_name] || Epics::DEFAULT_PRODUCT_NAME
self.current_order_id = options[:order_id] || 466560
@keyring = Epics::Keyring.new(options[:version] || Epics::Keyring::VERSION_25)
self.keys_content = keys_content.respond_to?(:read) ? keys_content.read : keys_content if keys_content
self.passphrase = passphrase
extract_keys if keys_content

yield self if block_given?
end

def version
keyring.version
end

def urn_schema
case version
when Epics::Keyring::VERSION_24
"http://www.ebics.org/#{version}"
when Epics::Keyring::VERSION_25, Epics::Keyring::VERSION_30
"urn:org:ebics:#{version}"
end
end

def inspect
"#<#{self.class}:#{self.object_id}
@version=#{self.keyring.version},
@keys=#{self.keys.keys},
@user_id=\"#{self.user_id}\",
@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
keyring.user_encryption&.version
end

def encryption_key
keyring.user_encryption&.key
end

def signature_version
keyring.user_signature&.version
end

def a
keys["A006"]
def signature_key
keyring.user_signature&.key
end

def x
keys["X002"]
def authentication_version
keyring.user_authentication&.version
end

def bank_e
keys["#{host_id.upcase}.E002"]
def authentication_key
keyring.user_authentication&.key
end

def bank_x
keys["#{host_id.upcase}.X002"]
def bank_encryption_key
keyring.bank_encryption&.key
end

def bank_encryption_version
keyring.bank_encryption&.version
end

def bank_authentication_key
keyring.bank_authentication&.key
end

def bank_authentication_version
keyring.bank_authentication&.version
end

def keys
user_signature = [keyring.user_signature, keyring.user_authentication, keyring.user_encryption].each_with_object({}) do |signature, keys|
keys[signature.version] = signature.key if signature
end
bank_signature = [keyring.bank_authentication, keyring.bank_encryption].each_with_object({}) do |signature, keys|
keys["#{host_id.upcase}.#{signature.version}"] = signature.key if signature
end

user_signature.merge(bank_signature)
end

def name
Expand All @@ -61,10 +118,25 @@ 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, options = {}, &block)
signature_version = options.delete(:signature_version) || Epics::Signature::A_VERSION_6
client = new(nil, passphrase, url, host_id, user_id, partner_id, options, &block)
[signature_version, Epics::Signature::X_VERSION_2, Epics::Signature::E_VERSION_2].each do |version|
signature = case version
when Epics::Signature::A_VERSION_6
Epics::Signature.new(version, Epics::SignatureAlgorithm::RsaPss.new(OpenSSL::PKey::RSA.generate(keysize)))
when Epics::Signature::A_VERSION_5, Epics::Signature::X_VERSION_2, Epics::Signature::E_VERSION_2
Epics::Signature.new(version, Epics::SignatureAlgorithm::RsaPkcs1.new(OpenSSL::PKey::RSA.generate(keysize)))
end

case signature.type
when Epics::Signature::TYPE_A
client.keyring.user_signature = signature
when Epics::Signature::TYPE_X
client.keyring.user_authentication = signature
when Epics::Signature::TYPE_E
client.keyring.user_encryption = signature
end
end

client
Expand Down Expand Up @@ -115,8 +187,8 @@ def HEV
end

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

modulus = Base64.decode64(node.at_xpath(".//*[local-name() = 'Modulus']").content)
exponent = Base64.decode64(node.at_xpath(".//*[local-name() = 'Exponent']").content)
Expand All @@ -126,11 +198,25 @@ def HPB
sequence << OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(exponent, 2))

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

self.keys["#{host_id.upcase}.#{type}"] = Epics::Key.new(bank)
signature = Epics::Signature.new(
signature_version,
case signature_version
when Epics::Signature::E_VERSION_2, Epics::Signature::X_VERSION_2
Epics::SignatureAlgorithm::RsaPkcs1.new(bank)
end
)

case signature.type
when Epics::Signature::TYPE_E
keyring.bank_encryption = signature
when Epics::Signature::TYPE_X
keyring.bank_authentication = signature
end
rescue Epics::Signature::UnknownTypeError
rescue Epics::Signature::UnknownVersionError
end

[bank_x, bank_e]
[bank_authentication_key, bank_encryption_key]
end

def AZV(document)
Expand Down Expand Up @@ -238,15 +324,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 @@ -278,14 +364,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 @@ -309,7 +393,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 @@ -318,13 +402,48 @@ def connection
end

def extract_keys
JSON.load(self.keys_content).each_with_object({}) do |(type, key), memo|
memo[type] = Epics::Key.new(decrypt(key)) if key
JSON.load(self.keys_content).each do |signature_version, key|
next unless key

is_bank_key = signature_version.start_with?("#{host_id.upcase}.")
signature_version = signature_version.sub("#{host_id.upcase}.", '') if is_bank_key

signature = Epics::Signature.new(
signature_version,
case signature_version
when Epics::Signature::A_VERSION_6
Epics::SignatureAlgorithm::RsaPss.new(decrypt(key))
when Epics::Signature::A_VERSION_5, Epics::Signature::E_VERSION_2, Epics::Signature::X_VERSION_2
Epics::SignatureAlgorithm::RsaPkcs1.new(decrypt(key))
end
)

if is_bank_key
case signature.type
when Epics::Signature::TYPE_X
keyring.bank_authentication = signature
when Epics::Signature::TYPE_E
keyring.bank_encryption = signature
end
else
case signature.type
when Epics::Signature::TYPE_A
keyring.user_signature = signature
when Epics::Signature::TYPE_X
keyring.user_authentication = signature
when Epics::Signature::TYPE_E
keyring.user_encryption = signature
end
end
rescue Epics::Signature::UnknownTypeError
rescue Epics::Signature::UnknownVersionError
end
end

def dump_keys
JSON.dump(keys.each_with_object({}) {|(k,v),m| m[k]= encrypt(v.key.to_pem)})
JSON.pretty_generate(keys.each_with_object({}) do |(version, signature), keys|
keys[version] = encrypt(signature.key.to_pem)
end, JSON.dump_default_options)
end

def new_cipher
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
Loading