diff --git a/CHANGELOG.md b/CHANGELOG.md index c9d9b46..90d0d88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/lib/epics/client.rb b/lib/epics/client.rb index c71bc30..fc2999f 100644 --- a/lib/epics/client.rb +++ b/lib/epics/client.rb @@ -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| diff --git a/lib/epics/response.rb b/lib/epics/response.rb index 7ac9873..2f07a53 100644 --- a/lib/epics/response.rb +++ b/lib/epics/response.rb @@ -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") @@ -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 diff --git a/lib/epics/version.rb b/lib/epics/version.rb index d9c5642..80f6d8c 100644 --- a/lib/epics/version.rb +++ b/lib/epics/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Epics - VERSION = '2.11.0' + VERSION = '2.12.0' end