Skip to content
Open
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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
### Unreleased

## [2.12.0]

### Fixed
- Fixed multi-segment download handling for C53 and other order types
- Transaction key is now properly cached from first segment and reused
- Segments are combined before decryption (matching EBICS specification)
- Added proper support for zero IV in AES-128-CBC decryption

### Changed
- Refactored download method to better handle multi-segment responses

### 2.11.0

- [ENHANCEMENT] Added FUL order type (thanks to @scollon-pl)
Expand Down
91 changes: 85 additions & 6 deletions lib/epics/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -317,17 +317,96 @@ def upload(order_type, document)
return res.transaction_id, [res.order_id, order_id].detect { |id| id.to_s.chars.any? }
end

def download(order_type, *args, **options)
document = order_type.new(self, *args, **options)
res = post(url, document.to_xml).body
document.transaction_id = res.transaction_id

def download(order_type, *args, **options)
document = order_type.new(self, *args, **options)
res = post(url, document.to_xml).body
document.transaction_id = res.transaction_id

if res.segmented? && !res.last_segment?
puts "[#{order_type}] Multi-segment download detected" if debug_mode

# Step 1: Get and decrypt transaction key from first segment
transaction_key = res.transaction_key
if transaction_key.nil?
puts "[#{order_type}] ERROR: No transaction key in first segment!" if debug_mode
return nil
end
puts "[#{order_type}] Transaction key obtained: #{transaction_key.bytesize} bytes" if debug_mode

# Array to store binary segments (NOT decrypted)
binary_segments = []

# Step 2: Get binary data from first segment (NOT decrypted)
first_segment_binary = res.order_data_binary
binary_segments << first_segment_binary if first_segment_binary
puts "[#{order_type}] Segment 1 binary: #{first_segment_binary&.bytesize} bytes" if debug_mode

current_segment = 2
total_segments = res.num_segments
max_segments = total_segments > 0 ? total_segments : 100

# Step 3: Fetch all other segments as binary (NOT decrypted)
while current_segment <= max_segments
puts "[#{order_type}] Fetching segment #{current_segment}/#{max_segments}..." if debug_mode

segment_xml = document.to_segment_request_xml(current_segment)
segment_res = post(url, segment_xml).body

if segment_res.technical_error? || segment_res.business_error?
puts "[#{order_type}] Error in segment #{current_segment}" if debug_mode
break
end

# Get binary data (NOT decrypted)
segment_binary = segment_res.order_data_binary
binary_segments << segment_binary if segment_binary
puts "[#{order_type}] Segment #{current_segment} binary: #{segment_binary&.bytesize} bytes" if debug_mode

if segment_res.last_segment?
puts "[#{order_type}] Last segment reached" if debug_mode
post(url, document.to_receipt_xml).body
break
end

current_segment += 1
end

# Step 4: Combine all binary segments
combined_encrypted_data = binary_segments.compact.join
puts "[#{order_type}] Combined encrypted binary: #{combined_encrypted_data.bytesize} bytes" if debug_mode

# Step 5: Decrypt the combined data with transaction key
begin
cipher = OpenSSL::Cipher.new("aes-128-cbc")
cipher.decrypt
cipher.padding = 0
cipher.key = transaction_key
cipher.iv = "\x00" * 16 # EBICS uses zero IV

decrypted_data = cipher.update(combined_encrypted_data) + cipher.final
puts "[#{order_type}] Decrypted combined data: #{decrypted_data.bytesize} bytes" if debug_mode

# Step 6: Decompress the decrypted data
decompressed = Zlib::Inflate.new.inflate(decrypted_data)
puts "[#{order_type}] Final decompressed data: #{decompressed.bytesize} bytes" if debug_mode
decompressed

rescue OpenSSL::Cipher::CipherError => e
puts "[#{order_type}] Decryption error: #{e.message}" if debug_mode
nil
rescue Zlib::DataError => e
puts "[#{order_type}] Decompression error: #{e.message}" if debug_mode
# Return decrypted data if decompression fails
decrypted_data
end
else
# Single segment - use normal order_data
if res.segmented? && res.last_segment?
post(url, document.to_receipt_xml).body
end

res.order_data
end
end

def download_and_unzip(order_type, *args, **options)
[].tap do |entries|
Expand Down
82 changes: 73 additions & 9 deletions lib/epics/response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,38 @@ def public_digest_valid?
client.e.public_digest == encryption_pub_key_digest.content
end

def order_data
order_data_encrypted = Base64.decode64(doc.xpath("//xmlns:OrderData", xmlns: 'urn:org:ebics:H004').first.content)

data = (cipher.update(order_data_encrypted) + cipher.final)

def order_data
order_data_elements = doc.xpath("//xmlns:OrderData", xmlns: 'urn:org:ebics:H004')

if order_data_elements.empty?
puts "DEBUG: No OrderData element found in EBICS response" if client.debug_mode
return nil
end

order_data_element = order_data_elements.first

if order_data_element.content.nil? || order_data_element.content.empty?
puts "DEBUG: OrderData element found but content is empty" if client.debug_mode
return nil
end

begin
# For single segment: decode base64 → decrypt → decompress
order_data_encrypted = Base64.decode64(order_data_element.content)

cipher_obj = cipher
if cipher_obj.nil?
puts "DEBUG: Cannot decrypt - cipher is nil (missing transaction key)" if client.debug_mode
return nil
end

data = cipher_obj.update(order_data_encrypted) + cipher_obj.final
Zlib::Inflate.new.inflate(data)
rescue => e
puts "DEBUG: Error processing order data: #{e.message}" if client.debug_mode
nil
end
end

def cipher
cipher = OpenSSL::Cipher.new("aes-128-cbc")
Expand All @@ -100,11 +125,50 @@ def cipher
cipher
end

def transaction_key
transaction_key_encrypted = Base64.decode64(doc.xpath("//xmlns:TransactionKey", xmlns: 'urn:org:ebics:H004').first.content)

@transaction_key ||= client.e.key.private_decrypt(transaction_key_encrypted)
def transaction_key
# Return cached key if already extracted
return @transaction_key if defined?(@transaction_key) && @transaction_key

transaction_key_elements = doc.xpath("//xmlns:TransactionKey", xmlns: 'urn:org:ebics:H004')

if transaction_key_elements.empty?
puts "DEBUG: No TransactionKey element found"
return nil
end

transaction_key_element = transaction_key_elements.first

if transaction_key_element.content.nil? || transaction_key_element.content.empty?
puts "DEBUG: TransactionKey element found but content is empty"
return nil
end

begin
# Convert base64 to binary array
transaction_key_encrypted = Base64.decode64(transaction_key_element.content)
puts "DEBUG: TransactionKey encrypted binary size: #{transaction_key_encrypted.bytesize} bytes" if client.debug_mode

# Decrypt with private key
@transaction_key = client.e.key.private_decrypt(transaction_key_encrypted)
puts "DEBUG: TransactionKey decrypted size: #{@transaction_key.bytesize} bytes" if client.debug_mode
@transaction_key
rescue => e
puts "DEBUG: Error decrypting transaction key: #{e.message}"
nil
end
end

def order_data_binary
order_data_element = doc.xpath("//xmlns:OrderData", xmlns: 'urn:org:ebics:H004').first
return nil unless order_data_element

# Just decode base64 to binary, NO decryption
order_data_binary = Base64.decode64(order_data_element.content)
return nil if order_data_binary.empty?

puts "DEBUG: Segment binary data: #{order_data_binary.bytesize} bytes" if client.debug_mode
order_data_binary
end

def digester
@digester ||= OpenSSL::Digest::SHA256.new
Expand Down
2 changes: 1 addition & 1 deletion lib/epics/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module Epics
VERSION = '2.11.0'
VERSION = '2.12.0'
end