epics 2.11.0 → 3.0.0.rc1

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.
Files changed (245) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -1
  3. data/.semaphore/semaphore.yml +17 -8
  4. data/CHANGELOG.md +93 -0
  5. data/EBICS_REFERENCES.md +169 -0
  6. data/README.md +219 -50
  7. data/lib/epics/azv.rb +3 -10
  8. data/lib/epics/b2b.rb +3 -10
  9. data/lib/epics/bka.rb +3 -14
  10. data/lib/epics/builders/body_builder/base.rb +26 -0
  11. data/lib/epics/builders/body_builder/v2.rb +9 -0
  12. data/lib/epics/builders/body_builder/v3.rb +9 -0
  13. data/lib/epics/builders/body_builder.rb +6 -0
  14. data/lib/epics/builders/data_encryption_info_builder.rb +29 -0
  15. data/lib/epics/builders/data_transfer_builder/base.rb +52 -0
  16. data/lib/epics/builders/data_transfer_builder/v2.rb +9 -0
  17. data/lib/epics/builders/data_transfer_builder/v3.rb +12 -0
  18. data/lib/epics/builders/data_transfer_builder.rb +6 -0
  19. data/lib/epics/builders/header_builder/base.rb +26 -0
  20. data/lib/epics/builders/header_builder/v2.rb +9 -0
  21. data/lib/epics/builders/header_builder/v3.rb +9 -0
  22. data/lib/epics/builders/header_builder.rb +6 -0
  23. data/lib/epics/builders/mutable_builder.rb +28 -0
  24. data/lib/epics/builders/order_details_builder/base.rb +100 -0
  25. data/lib/epics/builders/order_details_builder/v2.rb +21 -0
  26. data/lib/epics/builders/order_details_builder/v3.rb +51 -0
  27. data/lib/epics/builders/order_details_builder.rb +10 -0
  28. data/lib/epics/builders/request_builder.rb +25 -0
  29. data/lib/epics/builders/static_builder/base.rb +75 -0
  30. data/lib/epics/builders/static_builder/v2.rb +9 -0
  31. data/lib/epics/builders/static_builder/v3.rb +9 -0
  32. data/lib/epics/builders/static_builder.rb +9 -0
  33. data/lib/epics/builders/transfer_receipt_builder.rb +19 -0
  34. data/lib/epics/builders/xml_builder/base.rb +92 -0
  35. data/lib/epics/builders/xml_builder/v24.rb +31 -0
  36. data/lib/epics/builders/xml_builder/v25.rb +31 -0
  37. data/lib/epics/builders/xml_builder/v3.rb +31 -0
  38. data/lib/epics/builders/xml_builder.rb +7 -0
  39. data/lib/epics/builders.rb +13 -0
  40. data/lib/epics/c2s.rb +3 -10
  41. data/lib/epics/c52.rb +2 -14
  42. data/lib/epics/c53.rb +2 -14
  43. data/lib/epics/c54.rb +2 -14
  44. data/lib/epics/c5n.rb +3 -14
  45. data/lib/epics/ccs.rb +3 -10
  46. data/lib/epics/cct.rb +3 -10
  47. data/lib/epics/cd1.rb +3 -10
  48. data/lib/epics/cdb.rb +3 -10
  49. data/lib/epics/cdd.rb +3 -10
  50. data/lib/epics/cds.rb +3 -10
  51. data/lib/epics/cdz.rb +3 -14
  52. data/lib/epics/cip.rb +3 -10
  53. data/lib/epics/client.rb +489 -125
  54. data/lib/epics/crypt/aes.rb +56 -0
  55. data/lib/epics/crypt/x_509.rb +52 -0
  56. data/lib/epics/crypt.rb +5 -0
  57. data/lib/epics/crz.rb +3 -14
  58. data/lib/epics/factories/crypt/aes_factory.rb +5 -0
  59. data/lib/epics/factories/crypt.rb +4 -0
  60. data/lib/epics/factories/request_factory/base.rb +236 -0
  61. data/lib/epics/factories/request_factory/v2.rb +194 -0
  62. data/lib/epics/factories/request_factory/v24.rb +32 -0
  63. data/lib/epics/factories/request_factory/v25.rb +31 -0
  64. data/lib/epics/factories/request_factory/v3.rb +305 -0
  65. data/lib/epics/factories/request_factory.rb +8 -0
  66. data/lib/epics/factories.rb +5 -0
  67. data/lib/epics/fdl.rb +3 -18
  68. data/lib/epics/ful.rb +3 -15
  69. data/lib/epics/generic_request.rb +31 -92
  70. data/lib/epics/generic_upload_request.rb +14 -67
  71. data/lib/epics/haa.rb +3 -9
  72. data/lib/epics/hac.rb +3 -14
  73. data/lib/epics/handlers/auth_signature_handler.rb +102 -0
  74. data/lib/epics/handlers/order_data_handler/base.rb +98 -0
  75. data/lib/epics/handlers/order_data_handler/v2.rb +39 -0
  76. data/lib/epics/handlers/order_data_handler/v24.rb +11 -0
  77. data/lib/epics/handlers/order_data_handler/v25.rb +11 -0
  78. data/lib/epics/handlers/order_data_handler/v3.rb +41 -0
  79. data/lib/epics/handlers/order_data_handler.rb +8 -0
  80. data/lib/epics/handlers/user_signature_handler/base.rb +10 -0
  81. data/lib/epics/handlers/user_signature_handler/v2.rb +17 -0
  82. data/lib/epics/handlers/user_signature_handler/v3.rb +17 -0
  83. data/lib/epics/handlers/user_signature_handler.rb +6 -0
  84. data/lib/epics/handlers.rb +6 -0
  85. data/lib/epics/hev.rb +2 -15
  86. data/lib/epics/hia.rb +4 -55
  87. data/lib/epics/hkd.rb +3 -9
  88. data/lib/epics/hpb.rb +3 -13
  89. data/lib/epics/hpd.rb +3 -9
  90. data/lib/epics/htd.rb +3 -9
  91. data/lib/epics/ini.rb +4 -46
  92. data/lib/epics/keyring.rb +17 -0
  93. data/lib/epics/letter_renderer.rb +17 -22
  94. data/lib/epics/ptk.rb +3 -14
  95. data/lib/epics/response.rb +34 -29
  96. data/lib/epics/services/crypt_service.rb +62 -0
  97. data/lib/epics/services/digest_resolver/base.rb +30 -0
  98. data/lib/epics/services/digest_resolver/v2.rb +13 -0
  99. data/lib/epics/services/digest_resolver/v3.rb +19 -0
  100. data/lib/epics/services/digest_resolver.rb +6 -0
  101. data/lib/epics/services.rb +5 -0
  102. data/lib/epics/signature.rb +50 -0
  103. data/lib/epics/{key.rb → signature_algorithm/base.rb} +14 -21
  104. data/lib/epics/signature_algorithm/rsa.rb +50 -0
  105. data/lib/epics/signature_algorithm/rsapkcs1.rb +16 -0
  106. data/lib/epics/signature_algorithm/rsapss.rb +20 -0
  107. data/lib/epics/signature_algorithm.rb +2 -0
  108. data/lib/epics/sta.rb +3 -14
  109. data/lib/epics/version.rb +1 -1
  110. data/lib/epics/vmk.rb +3 -14
  111. data/lib/epics/wss.rb +3 -9
  112. data/lib/epics/xct.rb +3 -10
  113. data/lib/epics/xds.rb +3 -10
  114. data/lib/epics/xe2.rb +3 -10
  115. data/lib/epics/xe3.rb +3 -10
  116. data/lib/epics/xek.rb +5 -0
  117. data/lib/epics/yct.rb +6 -0
  118. data/lib/epics/z01.rb +2 -14
  119. data/lib/epics/z52.rb +2 -14
  120. data/lib/epics/z53.rb +2 -14
  121. data/lib/epics/z54.rb +2 -14
  122. data/lib/epics/zsr.rb +5 -0
  123. data/lib/epics.rb +68 -5
  124. data/lib/letter/ini.erb +27 -6
  125. data/spec/aes_spec.rb +46 -0
  126. data/spec/auth_signature_handler_spec.rb +58 -0
  127. data/spec/builders_spec.rb +616 -0
  128. data/spec/certificate_handling_spec.rb +189 -0
  129. data/spec/client_setup_block_spec.rb +41 -0
  130. data/spec/client_spec.rb +143 -91
  131. data/spec/crypt_service_spec.rb +132 -0
  132. data/spec/date_range_spec.rb +46 -0
  133. data/spec/digest_resolver_spec.rb +164 -0
  134. data/spec/fixtures/bank_x509.pem +3 -0
  135. data/spec/fixtures/xml/haa.xml +40 -1
  136. data/spec/fixtures/xml/haa_receipt.xml +33 -30
  137. data/spec/fixtures/xml/haa_response_h004.xml +4 -0
  138. data/spec/fixtures/xml/haa_response_h005.xml +38 -0
  139. data/spec/fixtures/xml/hia_request_order_data.xml +24 -1
  140. data/spec/fixtures/xml/hpb_request.xml +1 -1
  141. data/spec/fixtures/xml/hpb_response_x509.xml +16 -0
  142. data/spec/fixtures/xml/htd_order_data_h005.xml +55 -0
  143. data/spec/fixtures/xml/jruby/hia.xml +20 -22
  144. data/spec/fixtures/xml/jruby/ini.xml +1 -3
  145. data/spec/fixtures/xml/ruby/hia.xml +21 -1
  146. data/spec/fixtures/xml/ruby/ini.xml +21 -1
  147. data/spec/handlers_spec.rb +267 -0
  148. data/spec/hpb_malformed_certificate_spec.rb +36 -0
  149. data/spec/key_file_decryption_spec.rb +27 -0
  150. data/spec/key_file_preservation_spec.rb +78 -0
  151. data/spec/keyring_spec.rb +349 -0
  152. data/spec/middleware/parse_ebics_spec.rb +2 -1
  153. data/spec/order_id_spec.rb +65 -0
  154. data/spec/orders/azv_spec.rb +63 -7
  155. data/spec/orders/b2b_spec.rb +68 -3
  156. data/spec/orders/bka_spec.rb +34 -4
  157. data/spec/orders/c2s_spec.rb +62 -9
  158. data/spec/orders/c52_spec.rb +34 -4
  159. data/spec/orders/c53_spec.rb +34 -4
  160. data/spec/orders/c54_spec.rb +36 -4
  161. data/spec/orders/c5n_spec.rb +36 -4
  162. data/spec/orders/camt_parity_spec.rb +80 -0
  163. data/spec/orders/ccs_spec.rb +62 -9
  164. data/spec/orders/cct_spec.rb +83 -9
  165. data/spec/orders/cd1_spec.rb +50 -7
  166. data/spec/orders/cdb_spec.rb +64 -7
  167. data/spec/orders/cdd_spec.rb +64 -7
  168. data/spec/orders/cds_spec.rb +74 -0
  169. data/spec/orders/cdz_spec.rb +67 -4
  170. data/spec/orders/cip_spec.rb +61 -9
  171. data/spec/orders/crz_spec.rb +67 -4
  172. data/spec/orders/fdl_spec.rb +143 -13
  173. data/spec/orders/ful_spec.rb +51 -10
  174. data/spec/orders/haa_spec.rb +115 -9
  175. data/spec/orders/hac_spec.rb +89 -10
  176. data/spec/orders/hev_spec.rb +1 -6
  177. data/spec/orders/hia_spec.rb +25 -22
  178. data/spec/orders/hkd_spec.rb +23 -4
  179. data/spec/orders/hpb_spec.rb +7 -4
  180. data/spec/orders/hpd_spec.rb +23 -4
  181. data/spec/orders/htd_spec.rb +23 -4
  182. data/spec/orders/ini_spec.rb +23 -18
  183. data/spec/orders/ptk_spec.rb +27 -4
  184. data/spec/orders/sta_spec.rb +129 -11
  185. data/spec/orders/vmk_spec.rb +36 -4
  186. data/spec/orders/wss_spec.rb +29 -3
  187. data/spec/orders/x_509_certificate_spec.rb +9 -1
  188. data/spec/orders/xct_spec.rb +63 -0
  189. data/spec/orders/xds_spec.rb +13 -6
  190. data/spec/orders/xe2_spec.rb +64 -7
  191. data/spec/orders/xe3_spec.rb +64 -7
  192. data/spec/orders/xek_spec.rb +31 -0
  193. data/spec/orders/yct_spec.rb +43 -0
  194. data/spec/orders/z01_spec.rb +34 -4
  195. data/spec/orders/z52_spec.rb +34 -4
  196. data/spec/orders/z53_spec.rb +34 -4
  197. data/spec/orders/z54_spec.rb +36 -4
  198. data/spec/orders/zsr_spec.rb +31 -0
  199. data/spec/request_factory_spec.rb +444 -0
  200. data/spec/response_business_error_spec.rb +45 -0
  201. data/spec/response_validation_spec.rb +23 -0
  202. data/spec/{key_spec.rb → signature_algorithm/rsa_pss_spec.rb} +4 -13
  203. data/spec/support/ebics_matcher.rb +17 -9
  204. data/spec/support/ebics_shared_examples.rb +642 -0
  205. data/spec/support/h005_client_helper.rb +23 -0
  206. data/spec/upload_digest_spec.rb +69 -0
  207. data/spec/user_signature_handler_spec.rb +25 -0
  208. data/spec/version_support_error_spec.rb +76 -0
  209. data/spec/xsd/{ebics_hev.xsd → H000/ebics_hev.xsd} +0 -0
  210. data/spec/xsd/H003/ebics.xsd +11 -0
  211. data/spec/xsd/H003/ebics_keymgmt_request.xsd +555 -0
  212. data/spec/xsd/H003/ebics_keymgmt_response.xsd +131 -0
  213. data/spec/xsd/H003/ebics_orders.xsd +1811 -0
  214. data/spec/xsd/H003/ebics_request.xsd +354 -0
  215. data/spec/xsd/H003/ebics_response.xsd +161 -0
  216. data/spec/xsd/{ebics_signature.xsd → H003/ebics_signature.xsd} +0 -0
  217. data/spec/xsd/H003/ebics_types.xsd +2361 -0
  218. data/spec/xsd/H003/xmldsig-core-schema.xsd +318 -0
  219. data/spec/xsd/H004/ebics_signature.xsd +217 -0
  220. data/spec/xsd/H005/ebics_H005.xsd +11 -0
  221. data/spec/xsd/H005/ebics_keymgmt_request_H005.xsd +523 -0
  222. data/spec/xsd/H005/ebics_keymgmt_response_H005.xsd +137 -0
  223. data/spec/xsd/H005/ebics_orders_H005.xsd +2094 -0
  224. data/spec/xsd/H005/ebics_request_H005.xsd +349 -0
  225. data/spec/xsd/H005/ebics_response_H005.xsd +167 -0
  226. data/spec/xsd/H005/ebics_signature_S002.xsd +177 -0
  227. data/spec/xsd/H005/ebics_types_H005.xsd +1885 -0
  228. data/spec/xsd/H005/xmldsig-core-schema.xsd +318 -0
  229. metadata +197 -36
  230. data/lib/epics/header_request.rb +0 -58
  231. data/lib/epics/middleware/xmlsig.rb +0 -16
  232. data/lib/epics/signer.rb +0 -40
  233. data/lib/epics/x_509_certificate.rb +0 -15
  234. data/lib/letter/ini_with_certs.erb +0 -335
  235. data/spec/generic_upload_spec.rb +0 -58
  236. data/spec/hpb_spec.rb +0 -15
  237. data/spec/signer_spec.rb +0 -34
  238. /data/spec/xsd/{ebics_H004.xsd → H004/ebics_H004.xsd} +0 -0
  239. /data/spec/xsd/{ebics_keymgmt_request_H004.xsd → H004/ebics_keymgmt_request_H004.xsd} +0 -0
  240. /data/spec/xsd/{ebics_keymgmt_response_H004.xsd → H004/ebics_keymgmt_response_H004.xsd} +0 -0
  241. /data/spec/xsd/{ebics_orders_H004.xsd → H004/ebics_orders_H004.xsd} +0 -0
  242. /data/spec/xsd/{ebics_request_H004.xsd → H004/ebics_request_H004.xsd} +0 -0
  243. /data/spec/xsd/{ebics_response_H004.xsd → H004/ebics_response_H004.xsd} +0 -0
  244. /data/spec/xsd/{ebics_types_H004.xsd → H004/ebics_types_H004.xsd} +0 -0
  245. /data/spec/xsd/{xmldsig-core-schema.xsd → H004/xmldsig-core-schema.xsd} +0 -0
data/lib/epics/client.rb CHANGED
@@ -1,80 +1,251 @@
1
1
  class Epics::Client
2
2
  extend Forwardable
3
3
 
4
- attr_accessor :passphrase, :url, :host_id, :user_id, :partner_id, :keys, :keys_content, :locale, :product_name,
5
- :x_509_certificates_content, :debug_mode
6
-
4
+ attr_accessor :passphrase, :url, :host_id, :user_id, :partner_id, :keys_content, :locale, :product_name,
5
+ :debug_mode
6
+ attr_reader :current_order_id
7
+ attr_reader :keyring
7
8
  attr_writer :iban, :bic, :name
8
-
9
+
9
10
  def_delegators :connection, :post
10
-
11
+
12
+ USER_AGENT = "EPICS v#{Epics::VERSION}"
13
+
14
+ # EBICS 3.0 requires at least 2048 bits.
15
+ DEFAULT_KEY_SIZE = 4096
16
+
17
+ # OrderIDType is `[A-Z][A-Z0-9]{3}`, i.e. base 36 from A000 to ZZZZ. Only EBICS 2.4
18
+ # sends an OrderID; 2.5 and 3.0 leave numbering to the bank.
19
+ MIN_ORDER_ID = 466_560
20
+ MAX_ORDER_ID = 1_679_615
21
+ ORDER_ID_FORMAT = /\A[A-Z][A-Z0-9]{3}\z/
22
+
23
+ # Constructor option carrying certificate content, per signature type.
24
+ CERTIFICATE_OPTIONS = {
25
+ Epics::Signature::TYPE_A => :x_509_certificate_a_content,
26
+ Epics::Signature::TYPE_X => :x_509_certificate_x_content,
27
+ Epics::Signature::TYPE_E => :x_509_certificate_e_content
28
+ }.freeze
29
+
11
30
  def initialize(keys_content, passphrase, url, host_id, user_id, partner_id, options = {})
12
- self.keys_content = keys_content.respond_to?(:read) ? keys_content.read : keys_content if keys_content
13
- self.passphrase = passphrase
14
- self.keys = extract_keys if keys_content
15
- self.url = url
31
+ self.url = url
16
32
  self.host_id = host_id
17
33
  self.user_id = user_id
18
34
  self.partner_id = partner_id
19
35
  self.locale = options[:locale] || Epics::DEFAULT_LOCALE
20
36
  self.product_name = options[:product_name] || Epics::DEFAULT_PRODUCT_NAME
37
+ self.current_order_id = options[:order_id] || MIN_ORDER_ID
38
+ @keyring = Epics::Keyring.new(options[:version] || Epics::Keyring::VERSION_25)
39
+ self.keys_content = keys_content.respond_to?(:read) ? keys_content.read : keys_content if keys_content
40
+ self.passphrase = passphrase
21
41
  self.debug_mode = !!options[:debug_mode]
22
- self.x_509_certificates_content = {
23
- a: options[:x_509_certificate_a_content],
24
- x: options[:x_509_certificate_x_content],
25
- e: options[:x_509_certificate_e_content]
26
- }
42
+ @certificate_contents = CERTIFICATE_OPTIONS.transform_values { |option| options[option] }
43
+ @certificate_subject = options[:certificate_subject]
44
+ @certificate_not_after = options[:certificate_not_after]
45
+ @generate_certificates = options.fetch(:generate_certificates, keyring.version == Epics::Keyring::VERSION_30)
46
+ extract_keys if keys_content
47
+ apply_certificates
48
+
49
+ yield self if block_given?
50
+ end
51
+
52
+ # Attaches the certificates passed to the constructor. `.setup` calls this again
53
+ # because it builds the signatures after `#initialize` has run. Only assigns where
54
+ # content was given, so a certificate loaded from a key file survives.
55
+ def apply_certificates
56
+ user_signatures.each do |type, signature|
57
+ certificate = parse_certificate(@certificate_contents[type], CERTIFICATE_OPTIONS[type])
58
+ signature.certificate = certificate if certificate
59
+ end
60
+
61
+ self
62
+ end
63
+
64
+ # H005 cannot be initialised without certificates, so `.setup` mints them by default.
65
+ def generate_certificates?
66
+ @generate_certificates
67
+ end
68
+
69
+ # Self-signed certificate for each user key that lacks one. Permitted for INI/HIA by
70
+ # the Swiss market practice guidelines §6.1; banks requiring CA-issued certificates
71
+ # should be given `x_509_certificate_*_content`, which takes precedence.
72
+ #
73
+ # Called only by `.setup`: regenerating on every `.new` would hand the bank a
74
+ # different fingerprint than the one on the initialisation letter.
75
+ def generate_certificates
76
+ user_signatures.each_value do |signature|
77
+ next if signature.certificate
78
+
79
+ signature.certificate = Epics::Crypt::X509.generate(
80
+ signature.key,
81
+ subject: certificate_subject,
82
+ not_after: @certificate_not_after || Epics::Crypt::X509::UNLIMITED
83
+ )
84
+ end
85
+
86
+ self
87
+ end
88
+
89
+ # Built as an OpenSSL::X509::Name so IDs containing `/` or `=` need no escaping.
90
+ def certificate_subject
91
+ @certificate_subject ||= OpenSSL::X509::Name.new([
92
+ ['CN', user_id],
93
+ ['OU', partner_id],
94
+ ['O', host_id]
95
+ ])
96
+ end
97
+
98
+ def version
99
+ keyring.version
100
+ end
101
+
102
+ def urn_schema
103
+ case version
104
+ when Epics::Keyring::VERSION_24
105
+ "http://www.ebics.org/#{version}"
106
+ when Epics::Keyring::VERSION_25, Epics::Keyring::VERSION_30
107
+ "urn:org:ebics:#{version}"
108
+ end
27
109
  end
28
110
 
29
111
  def inspect
30
- "#<#{self.class}:#{self.object_id}
31
- @keys=#{self.keys.keys},
32
- @user_id=\"#{self.user_id}\",
33
- @partner_id=\"#{self.partner_id}\""
112
+ "#<#{self.class}:#{object_id}
113
+ @version=#{keyring.version},
114
+ @keys=#{keys.keys},
115
+ @user_id=\"#{user_id}\",
116
+ @partner_id=\"#{partner_id}\""
117
+ end
118
+
119
+ # Accepts either the protocol form ('A000') or the integer it encodes, so a caller
120
+ # can persist whichever it has and hand it back.
121
+ def current_order_id=(value)
122
+ # String#to_i(36) is lenient, so the format is checked before converting.
123
+ id = value.is_a?(String) && value.match?(ORDER_ID_FORMAT) ? value.to_i(36) : value
124
+
125
+ unless id.is_a?(Integer) && id.between?(MIN_ORDER_ID, MAX_ORDER_ID)
126
+ raise ArgumentError,
127
+ "order_id must be between #{MIN_ORDER_ID.to_s(36).upcase} and " \
128
+ "#{MAX_ORDER_ID.to_s(36).upcase}, got #{value.inspect}"
129
+ end
130
+
131
+ @current_order_id = id
132
+ end
133
+
134
+ # Not persisted with the keys: read it after use and pass it back as `order_id:` on
135
+ # the next client, or EBICS 2.4 uploads replay IDs the bank has already seen.
136
+ def next_order_id
137
+ raise 'Order ID overflow' if current_order_id >= MAX_ORDER_ID
138
+
139
+ self.current_order_id += 1
140
+ end
141
+
142
+ def encryption_version
143
+ keyring.user_encryption&.version
144
+ end
145
+
146
+ def encryption_key
147
+ keyring.user_encryption&.key
148
+ end
149
+
150
+ def signature_version
151
+ keyring.user_signature&.version
152
+ end
153
+
154
+ def signature_key
155
+ keyring.user_signature&.key
156
+ end
157
+
158
+ def authentication_version
159
+ keyring.user_authentication&.version
160
+ end
161
+
162
+ def authentication_key
163
+ keyring.user_authentication&.key
34
164
  end
35
165
 
36
- def e
37
- keys["E002"]
166
+ def bank_encryption_key
167
+ keyring.bank_encryption&.key
38
168
  end
39
169
 
40
- def a
41
- keys["A006"]
170
+ def bank_encryption_version
171
+ keyring.bank_encryption&.version
42
172
  end
43
173
 
44
- def x
45
- keys["X002"]
174
+ def bank_authentication_key
175
+ keyring.bank_authentication&.key
46
176
  end
47
177
 
48
- def bank_e
49
- keys["#{host_id.upcase}.E002"]
178
+ def bank_authentication_version
179
+ keyring.bank_authentication&.version
50
180
  end
51
181
 
52
- def bank_x
53
- keys["#{host_id.upcase}.X002"]
182
+ def keys
183
+ user_signature = [keyring.user_signature, keyring.user_authentication,
184
+ keyring.user_encryption].each_with_object({}) do |signature, keys|
185
+ keys[signature.version] = signature.key if signature
186
+ end
187
+ bank_signature = [keyring.bank_authentication, keyring.bank_encryption].each_with_object({}) do |signature, keys|
188
+ keys["#{host_id.upcase}.#{signature.version}"] = signature.key if signature
189
+ end
190
+
191
+ user_signature.merge(bank_signature)
54
192
  end
55
193
 
56
194
  def name
57
- @name ||= (self.HTD; @name)
195
+ @name ||= begin
196
+ self.HTD
197
+ @name
198
+ end
58
199
  end
59
200
 
60
201
  def iban
61
- @iban ||= (self.HTD; @iban)
202
+ @iban ||= begin
203
+ self.HTD
204
+ @iban
205
+ end
62
206
  end
63
207
 
64
208
  def bic
65
- @bic ||= (self.HTD; @bic)
209
+ @bic ||= begin
210
+ self.HTD
211
+ @bic
212
+ end
66
213
  end
67
214
 
68
215
  def order_types
69
- @order_types ||= (self.HTD; @order_types)
216
+ @order_types ||= begin
217
+ self.HTD
218
+ @order_types
219
+ end
70
220
  end
71
221
 
72
- def self.setup(passphrase, url, host_id, user_id, partner_id, keysize = 2048, options = {})
222
+ # Yields after the keys and certificates exist, so the block sees a usable client.
223
+ def self.setup(passphrase, url, host_id, user_id, partner_id, keysize = DEFAULT_KEY_SIZE, options = {})
224
+ options = options.dup
225
+ signature_version = options.delete(:signature_version) || Epics::Signature::A_VERSION_6
73
226
  client = new(nil, passphrase, url, host_id, user_id, partner_id, options)
74
- client.keys = %w(A006 X002 E002).each_with_object({}) do |type, memo|
75
- memo[type] = Epics::Key.new( OpenSSL::PKey::RSA.generate(keysize) )
227
+ [signature_version, Epics::Signature::X_VERSION_2, Epics::Signature::E_VERSION_2].each do |version|
228
+ signature = case version
229
+ when Epics::Signature::A_VERSION_6
230
+ Epics::Signature.new(version, Epics::SignatureAlgorithm::RsaPss.new(OpenSSL::PKey::RSA.generate(keysize)))
231
+ when Epics::Signature::A_VERSION_5, Epics::Signature::X_VERSION_2, Epics::Signature::E_VERSION_2
232
+ Epics::Signature.new(version, Epics::SignatureAlgorithm::RsaPkcs1.new(OpenSSL::PKey::RSA.generate(keysize)))
233
+ end
234
+
235
+ case signature.type
236
+ when Epics::Signature::TYPE_A
237
+ client.keyring.user_signature = signature
238
+ when Epics::Signature::TYPE_X
239
+ client.keyring.user_authentication = signature
240
+ when Epics::Signature::TYPE_E
241
+ client.keyring.user_encryption = signature
242
+ end
76
243
  end
77
244
 
245
+ client.apply_certificates
246
+ client.generate_certificates if client.generate_certificates?
247
+
248
+ yield client if block_given?
78
249
  client
79
250
  end
80
251
 
@@ -91,20 +262,16 @@ class Epics::Client
91
262
  path
92
263
  end
93
264
 
94
- def header_request
95
- @header_request ||= Epics::HeaderRequest.new(self)
96
- end
97
-
98
265
  def credit(document)
99
266
  self.CCT(document)
100
267
  end
101
268
 
102
269
  def debit(document, type = :CDD)
103
- self.public_send(type, document)
270
+ public_send(type, document)
104
271
  end
105
272
 
106
273
  def statements(from, to, type = :STA)
107
- self.public_send(type, from: from, to: to)
274
+ public_send(type, from: from, to: to)
108
275
  end
109
276
 
110
277
  def HIA
@@ -117,14 +284,17 @@ class Epics::Client
117
284
 
118
285
  def HEV
119
286
  res = post(url, Epics::HEV.new(self).to_xml).body
120
- res.doc.xpath("//xmlns:VersionNumber", xmlns: 'http://www.ebics.org/H000').each_with_object({}) do |node, versions|
287
+ res.doc.xpath('//xmlns:VersionNumber', xmlns: 'http://www.ebics.org/H000').each_with_object({}) do |node, versions|
121
288
  versions[node['ProtocolVersion']] = node.content
122
289
  end
123
290
  end
124
291
 
125
292
  def HPB
126
- Nokogiri::XML(download(Epics::HPB)).xpath("//xmlns:PubKeyValue", xmlns: "urn:org:ebics:H004").each do |node|
127
- type = node.parent.last_element_child.content
293
+ response_xml = download(Epics::HPB)
294
+ doc = Nokogiri::XML(response_xml)
295
+
296
+ doc.xpath('//xmlns:PubKeyValue', xmlns: urn_schema).each do |node|
297
+ signature_version = node.parent.last_element_child.content
128
298
 
129
299
  modulus = Base64.decode64(node.at_xpath(".//*[local-name() = 'Modulus']").content)
130
300
  exponent = Base64.decode64(node.at_xpath(".//*[local-name() = 'Exponent']").content)
@@ -134,67 +304,118 @@ class Epics::Client
134
304
  sequence << OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(exponent, 2))
135
305
 
136
306
  bank = OpenSSL::PKey::RSA.new(OpenSSL::ASN1::Sequence(sequence).to_der)
307
+ signature = Epics::Signature.new(
308
+ signature_version,
309
+ case signature_version
310
+ when Epics::Signature::E_VERSION_2, Epics::Signature::X_VERSION_2
311
+ Epics::SignatureAlgorithm::RsaPkcs1.new(bank)
312
+ end
313
+ )
314
+
315
+ case signature.type
316
+ when Epics::Signature::TYPE_E
317
+ keyring.bank_encryption = signature
318
+ when Epics::Signature::TYPE_X
319
+ keyring.bank_authentication = signature
320
+ end
321
+ rescue Epics::Signature::UnknownTypeError
322
+ rescue Epics::Signature::UnknownVersionError
323
+ end
137
324
 
138
- self.keys["#{host_id.upcase}.#{type}"] = Epics::Key.new(bank)
325
+ doc.xpath('//ds:X509Certificate', xmlns: urn_schema,
326
+ ds: 'http://www.w3.org/2000/09/xmldsig#').each do |cert_node|
327
+ cert_pem = "-----BEGIN CERTIFICATE-----\n#{cert_node.content}\n-----END CERTIFICATE-----"
328
+
329
+ info_element = cert_node.parent&.parent
330
+ auth_version = info_element&.at_xpath('.//*[local-name() = "AuthenticationVersion"]')&.content
331
+ enc_version = info_element&.at_xpath('.//*[local-name() = "EncryptionVersion"]')&.content
332
+ version = auth_version || enc_version
333
+
334
+ next unless version
335
+
336
+ begin
337
+ cert = OpenSSL::X509::Certificate.new(cert_pem)
338
+
339
+ signature = Epics::Signature.new(
340
+ version,
341
+ Epics::SignatureAlgorithm::RsaPkcs1.new(cert.public_key)
342
+ )
343
+ signature.certificate = Epics::Crypt::X509.new(cert_pem)
344
+
345
+ case signature.type
346
+ when Epics::Signature::TYPE_E
347
+ keyring.bank_encryption = signature
348
+ when Epics::Signature::TYPE_X
349
+ keyring.bank_authentication = signature
350
+ end
351
+ rescue Epics::Signature::UnknownTypeError
352
+ rescue Epics::Signature::UnknownVersionError
353
+ rescue OpenSSL::OpenSSLError => e
354
+ raise Epics::InvalidCertificateError.new('the HPB response', e)
355
+ end
139
356
  end
140
357
 
141
- [bank_x, bank_e]
358
+ [bank_authentication_key, bank_encryption_key]
359
+ end
360
+
361
+ def AZV(document, **options)
362
+ upload(Epics::AZV, document, **options)
142
363
  end
143
364
 
144
- def AZV(document)
145
- upload(Epics::AZV, document)
365
+ def CD1(document, **options)
366
+ upload(Epics::CD1, document, **options)
146
367
  end
147
368
 
148
- def CD1(document)
149
- upload(Epics::CD1, document)
369
+ def CDB(document, **options)
370
+ upload(Epics::CDB, document, **options)
150
371
  end
151
372
 
152
- def CDB(document)
153
- upload(Epics::CDB, document)
373
+ def C2S(document, **options)
374
+ upload(Epics::C2S, document, **options)
154
375
  end
155
376
 
156
- def C2S(document)
157
- upload(Epics::C2S, document)
377
+ def CDD(document, **options)
378
+ upload(Epics::CDD, document, **options)
158
379
  end
159
380
 
160
- def CDD(document)
161
- upload(Epics::CDD, document)
381
+ def XE2(document, **options)
382
+ upload(Epics::XE2, document, **options)
162
383
  end
163
384
 
164
- def XE2(document)
165
- upload(Epics::XE2, document)
385
+ def XE3(document, **options)
386
+ upload(Epics::XE3, document, **options)
166
387
  end
167
388
 
168
- def XE3(document)
169
- upload(Epics::XE3, document)
389
+ def CDS(document, **options)
390
+ upload(Epics::CDS, document, **options)
170
391
  end
171
392
 
172
- def CDS(document)
173
- upload(Epics::CDS, document)
393
+ def XDS(document, **options)
394
+ upload(Epics::XDS, document, **options)
174
395
  end
175
396
 
176
- def XDS(document)
177
- upload(Epics::XDS, document)
397
+ def CCT(document, **options)
398
+ upload(Epics::CCT, document, **options)
178
399
  end
179
400
 
180
- def CCT(document)
181
- upload(Epics::CCT, document)
401
+ def CIP(document, **options)
402
+ upload(Epics::CIP, document, **options)
182
403
  end
183
404
 
184
- def CIP(document)
185
- upload(Epics::CIP, document)
405
+ def CCS(document, **options)
406
+ upload(Epics::CCS, document, **options)
186
407
  end
187
408
 
188
- def CCS(document)
189
- upload(Epics::CCS, document)
409
+ def XCT(document, **options)
410
+ upload(Epics::XCT, document, **options)
190
411
  end
191
412
 
192
- def XCT(document)
193
- upload(Epics::XCT, document)
413
+ def YCT(document, **options)
414
+ upload(Epics::YCT, document, **options)
194
415
  end
195
416
 
196
- def FUL(document)
197
- upload(Epics::FUL, document)
417
+ def FUL(document, **options)
418
+ upload(Epics::FUL, document, **options)
198
419
  end
199
420
 
200
421
  def STA(from = nil, to = nil)
@@ -202,7 +423,7 @@ class Epics::Client
202
423
  end
203
424
 
204
425
  def FDL(format, from = nil, to = nil)
205
- download(Epics::FDL, file_format: format, from: from, to: to )
426
+ download(Epics::FDL, file_format: format, from: from, to: to)
206
427
  end
207
428
 
208
429
  def VMK(from = nil, to = nil)
@@ -221,48 +442,109 @@ class Epics::Client
221
442
  download_and_unzip(Epics::BKA, from: from, to: to)
222
443
  end
223
444
 
224
- def C52(from, to)
225
- download_and_unzip(Epics::C52, from: from, to: to)
445
+ def C52(from, to, scope: nil, msg_name_version: nil, container_type: nil)
446
+ download_and_unzip(Epics::C52, from: from, to: to, scope: scope, msg_name_version: msg_name_version,
447
+ container_type: container_type)
226
448
  end
227
449
 
228
- def C53(from, to)
229
- download_and_unzip(Epics::C53, from: from, to: to)
450
+ def C53(from, to, scope: nil, msg_name_version: nil, container_type: nil)
451
+ download_and_unzip(Epics::C53, from: from, to: to, scope: scope, msg_name_version: msg_name_version,
452
+ container_type: container_type)
230
453
  end
231
454
 
232
- def C54(from, to)
233
- download_and_unzip(Epics::C54, from: from, to: to)
455
+ def C54(from, to, scope: nil, msg_name_version: nil, container_type: nil)
456
+ download_and_unzip(Epics::C54, from: from, to: to, scope: scope, msg_name_version: msg_name_version,
457
+ container_type: container_type)
234
458
  end
235
459
 
236
460
  def C5N(from, to)
237
461
  download_and_unzip(Epics::C5N, from: from, to: to)
238
462
  end
239
463
 
240
- def Z01(from, to)
241
- download_and_unzip(Epics::Z01, from: from, to: to)
464
+ def Z01(from, to, scope: nil, service_option: nil, msg_name_version: nil, container_type: nil)
465
+ download_and_unzip(Epics::Z01, from: from, to: to, scope: scope, service_option: service_option,
466
+ msg_name_version: msg_name_version, container_type: container_type)
242
467
  end
243
468
 
244
- def Z52(from, to)
245
- download_and_unzip(Epics::Z52, from: from, to: to)
469
+ def Z52(from, to, scope: nil, service_option: nil, msg_name_version: nil, container_type: nil)
470
+ download_and_unzip(Epics::Z52, from: from, to: to, scope: scope, service_option: service_option,
471
+ msg_name_version: msg_name_version, container_type: container_type)
246
472
  end
247
473
 
248
- def Z53(from, to)
249
- download_and_unzip(Epics::Z53, from: from, to: to)
474
+ def Z53(from, to, scope: nil, service_option: nil, msg_name_version: nil, container_type: nil)
475
+ download_and_unzip(Epics::Z53, from: from, to: to, scope: scope, service_option: service_option,
476
+ msg_name_version: msg_name_version, container_type: container_type)
250
477
  end
251
478
 
252
- def Z54(from, to)
253
- download_and_unzip(Epics::Z54, from: from, to: to)
479
+ def Z54(from, to, scope: nil, service_option: nil, msg_name_version: nil, container_type: nil)
480
+ download_and_unzip(Epics::Z54, from: from, to: to, scope: scope, service_option: service_option,
481
+ msg_name_version: msg_name_version, container_type: container_type)
482
+ end
483
+
484
+ def XEK(from, to, scope: nil, service_option: nil, msg_name_version: nil, container_type: nil)
485
+ download_and_unzip(Epics::XEK, from: from, to: to, scope: scope, service_option: service_option,
486
+ msg_name_version: msg_name_version, container_type: container_type)
487
+ end
488
+
489
+ def ZSR(from, to, scope: nil, service_option: nil, msg_name_version: nil, container_type: nil)
490
+ download_and_unzip(Epics::ZSR, from: from, to: to, scope: scope, service_option: service_option,
491
+ msg_name_version: msg_name_version, container_type: container_type)
254
492
  end
255
493
 
256
494
  def HAA
257
- Nokogiri::XML(download(Epics::HAA)).at_xpath("//xmlns:OrderTypes", xmlns: "urn:org:ebics:H004").content.split(/\s/)
495
+ doc = Nokogiri::XML(download(Epics::HAA))
496
+ ns = { xmlns: urn_schema }
497
+
498
+ case version
499
+ when Epics::Keyring::VERSION_24, Epics::Keyring::VERSION_25
500
+ doc.at_xpath('//xmlns:OrderTypes', ns).content.split(/\s/)
501
+ when Epics::Keyring::VERSION_30
502
+ doc.xpath('//xmlns:Service', ns).map do |service|
503
+ {
504
+ service_name: service.at_xpath('xmlns:ServiceName', ns)&.text,
505
+ scope: service.at_xpath('xmlns:Scope', ns)&.text,
506
+ service_option: service.at_xpath('xmlns:ServiceOption', ns)&.text,
507
+ container: service.at_xpath('xmlns:Container', ns)&.[]('containerType'),
508
+ msg_name: service.at_xpath('xmlns:MsgName', ns)&.text
509
+ }.compact
510
+ end
511
+ end
258
512
  end
259
513
 
260
514
  def HTD
261
515
  Nokogiri::XML(download(Epics::HTD)).tap do |htd|
262
- @iban ||= htd.at_xpath("//xmlns:AccountNumber[@international='true']", xmlns: "urn:org:ebics:H004").text rescue nil
263
- @bic ||= htd.at_xpath("//xmlns:BankCode[@international='true']", xmlns: "urn:org:ebics:H004").text rescue nil
264
- @name ||= htd.at_xpath("//xmlns:Name", xmlns: "urn:org:ebics:H004").text rescue nil
265
- @order_types ||= htd.search("//xmlns:OrderTypes", xmlns: "urn:org:ebics:H004").map{|o| o.content.split(/\s/) }.delete_if{|o| o == ""}.flatten
516
+ @iban ||= begin
517
+ htd.at_xpath("//xmlns:AccountNumber[@international='true']", xmlns: urn_schema).text
518
+ rescue StandardError
519
+ nil
520
+ end
521
+ @bic ||= begin
522
+ htd.at_xpath("//xmlns:BankCode[@international='true']", xmlns: urn_schema).text
523
+ rescue StandardError
524
+ nil
525
+ end
526
+ @name ||= begin
527
+ htd.at_xpath('//xmlns:Name', xmlns: urn_schema).text
528
+ rescue StandardError
529
+ nil
530
+ end
531
+ @order_types ||= case version
532
+ when Epics::Keyring::VERSION_24, Epics::Keyring::VERSION_25
533
+ htd.search('//xmlns:OrderTypes', xmlns: urn_schema).map do |o|
534
+ o.content.split(/\s/)
535
+ end.delete_if { |o| o == '' }.flatten
536
+ when Epics::Keyring::VERSION_30
537
+ htd.xpath('//xmlns:Service', xmlns: urn_schema).map do |service|
538
+ ns = { xmlns: urn_schema }
539
+ {
540
+ service_name: service.at_xpath('xmlns:ServiceName', ns)&.text,
541
+ scope: service.at_xpath('xmlns:Scope', ns)&.text,
542
+ service_option: service.at_xpath('xmlns:ServiceOption', ns)&.text,
543
+ container: service.at_xpath('xmlns:Container', ns)&.[]('containerType'),
544
+ msg_name: service.at_xpath('xmlns:MsgName', ns)&.text
545
+ }.compact
546
+ end
547
+ end
266
548
  end.to_xml
267
549
  end
268
550
 
@@ -289,32 +571,33 @@ class Epics::Client
289
571
  def save_keys(path)
290
572
  File.write(path, dump_keys)
291
573
  end
292
-
293
- def x_509_certificate(type)
294
- content = x_509_certificates_content[type.to_sym]
295
- return if content.nil? || content.empty?
296
- Epics::X509Certificate.new(content)
297
- end
298
-
299
- def x_509_certificate_hash(type)
300
- content = x_509_certificates_content[type.to_sym]
301
- return if content.nil? || content.empty?
302
- cert = OpenSSL::X509::Certificate.new(content)
303
- Digest::SHA256.hexdigest(cert.to_der).upcase
304
- end
305
574
 
306
575
  private
307
576
 
308
- def upload(order_type, document)
309
- order = order_type.new(self, document)
310
- res = post(url, order.to_xml).body
311
- order.transaction_id = res.transaction_id
577
+ def user_signatures
578
+ {
579
+ Epics::Signature::TYPE_A => keyring.user_signature,
580
+ Epics::Signature::TYPE_X => keyring.user_authentication,
581
+ Epics::Signature::TYPE_E => keyring.user_encryption,
582
+ }.compact
583
+ end
584
+
585
+ def parse_certificate(content, option_name)
586
+ return if content.nil? || content.empty?
587
+
588
+ Epics::Crypt::X509.new(content)
589
+ rescue OpenSSL::X509::CertificateError => e
590
+ raise Epics::InvalidCertificateError.new(option_name, e)
591
+ end
312
592
 
313
- order_id = res.order_id
593
+ def upload(order_type, document, **options)
594
+ order = order_type.new(self, document, **options)
595
+ session = post(url, order.to_xml).body
596
+ order.transaction_id = session.transaction_id
314
597
 
315
598
  res = post(url, order.to_transfer_xml).body
316
599
 
317
- return res.transaction_id, [res.order_id, order_id].detect { |id| id.to_s.chars.any? }
600
+ [res.transaction_id, [res.order_id, session.order_id].detect { |id| id.to_s.chars.any? }]
318
601
  end
319
602
 
320
603
  def download(order_type, *args, **options)
@@ -322,9 +605,7 @@ class Epics::Client
322
605
  res = post(url, document.to_xml).body
323
606
  document.transaction_id = res.transaction_id
324
607
 
325
- if res.segmented? && res.last_segment?
326
- post(url, document.to_receipt_xml).body
327
- end
608
+ post(url, document.to_receipt_xml).body if res.segmented? && res.last_segment?
328
609
 
329
610
  res.order_data
330
611
  end
@@ -338,22 +619,99 @@ class Epics::Client
338
619
  end
339
620
 
340
621
  def connection
341
- @connection ||= Faraday.new(headers: { 'Content-Type' => 'text/xml', user_agent: "EPICS v#{Epics::VERSION}"}, ssl: { verify: verify_ssl? }) do |faraday|
342
- faraday.use Epics::XMLSIG, { client: self }
343
- faraday.use Epics::ParseEbics, { client: self}
622
+ @connection ||= Faraday.new(headers: { 'Content-Type' => 'text/xml', user_agent: USER_AGENT },
623
+ ssl: { verify: verify_ssl? }) do |faraday|
624
+ faraday.use Epics::ParseEbics, { client: self }
344
625
  # faraday.use MyAdapter
345
626
  faraday.response :logger, ::Logger.new(STDOUT), bodies: true if debug_mode # log requests/response to STDOUT
346
627
  end
347
628
  end
348
629
 
349
630
  def extract_keys
350
- JSON.load(self.keys_content).each_with_object({}) do |(type, key), memo|
351
- memo[type] = Epics::Key.new(decrypt(key)) if key
631
+ @unmapped_keys = {}
632
+
633
+ JSON.load(keys_content).each do |signature_version, value|
634
+ next unless value
635
+
636
+ entry_name = signature_version
637
+
638
+ # Handle new format: { "key" => "...", "cert" => "..." }
639
+ if value.is_a?(Hash)
640
+ key_pem = decrypt(value['key'])
641
+ cert_pem = value['cert'] ? decrypt(value['cert']) : nil
642
+ else
643
+ key_pem = decrypt(value)
644
+ cert_pem = nil
645
+ end
646
+
647
+ is_bank_key = signature_version.start_with?("#{host_id.upcase}.")
648
+ signature_version = signature_version.sub("#{host_id.upcase}.", '') if is_bank_key
649
+
650
+ signature = Epics::Signature.new(
651
+ signature_version,
652
+ case signature_version
653
+ when Epics::Signature::A_VERSION_6
654
+ Epics::SignatureAlgorithm::RsaPss.new(key_pem)
655
+ when Epics::Signature::A_VERSION_5, Epics::Signature::E_VERSION_2, Epics::Signature::X_VERSION_2
656
+ Epics::SignatureAlgorithm::RsaPkcs1.new(key_pem)
657
+ end
658
+ )
659
+
660
+ signature.certificate = Epics::Crypt::X509.new(cert_pem) if cert_pem
661
+
662
+ if is_bank_key
663
+ case signature.type
664
+ when Epics::Signature::TYPE_X
665
+ keyring.bank_authentication = signature
666
+ when Epics::Signature::TYPE_E
667
+ keyring.bank_encryption = signature
668
+ end
669
+ else
670
+ case signature.type
671
+ when Epics::Signature::TYPE_A
672
+ keyring.user_signature = signature
673
+ when Epics::Signature::TYPE_X
674
+ keyring.user_authentication = signature
675
+ when Epics::Signature::TYPE_E
676
+ keyring.user_encryption = signature
677
+ end
678
+ end
679
+ rescue Epics::Signature::UnknownTypeError, Epics::Signature::UnknownVersionError => e
680
+ # Kept verbatim so save_keys cannot drop it. A bank key whose prefix does not
681
+ # match host_id lands here, as does a key of a version this gem does not know.
682
+ @unmapped_keys[entry_name] = value
683
+ warn "[epics] WARNING: keeping #{entry_name.inspect} in the key file unchanged: #{e.message}"
352
684
  end
353
685
  end
354
686
 
355
687
  def dump_keys
356
- JSON.dump(keys.each_with_object({}) {|(k,v),m| m[k]= encrypt(v.key.to_pem)})
688
+ # Entries extract_keys could not place are carried through, so saving never
689
+ # discards part of the key file.
690
+ data = (@unmapped_keys || {}).dup
691
+
692
+ [keyring.user_signature, keyring.user_authentication,
693
+ keyring.user_encryption].each do |sig|
694
+ next if !sig
695
+
696
+ data[sig.version] = dump_signature(sig)
697
+ end
698
+
699
+ [keyring.bank_authentication, keyring.bank_encryption].each do |sig|
700
+ next if !sig
701
+
702
+ data["#{host_id.upcase}.#{sig.version}"] = dump_signature(sig)
703
+ end
704
+
705
+ JSON.pretty_generate(data, JSON.dump_default_options)
706
+ end
707
+
708
+ # Keys without a certificate keep the bare-string form so key files stay readable by
709
+ # older versions of the gem.
710
+ def dump_signature(signature)
711
+ key = encrypt(signature.key.key.to_pem)
712
+ return key if !signature.certificate
713
+
714
+ { 'key' => key, 'cert' => encrypt(signature.certificate.to_pem) }
357
715
  end
358
716
 
359
717
  def new_cipher
@@ -365,17 +723,23 @@ class Epics::Client
365
723
  def encrypt(data)
366
724
  salt = OpenSSL::Random.random_bytes(8)
367
725
 
368
- cipher = setup_cipher(:encrypt, self.passphrase, salt)
726
+ cipher = setup_cipher(:encrypt, passphrase, salt)
369
727
  Base64.strict_encode64([salt, cipher.update(data) + cipher.final].join)
370
728
  end
371
729
 
730
+ # Every value in a key file is a PEM. A wrong passphrase normally fails the padding
731
+ # check, but about one time in 256 it decrypts to garbage that happens to be padded
732
+ # correctly; without this check that surfaces later as an unrelated key parse error.
372
733
  def decrypt(data)
373
734
  data = Base64.strict_decode64(data)
374
735
  salt = data[0..7]
375
736
  data = data[8..-1]
376
737
 
377
- cipher = setup_cipher(:decrypt, self.passphrase, salt)
378
- cipher.update(data) + cipher.final
738
+ cipher = setup_cipher(:decrypt, passphrase, salt)
739
+ plaintext = cipher.update(data) + cipher.final
740
+ raise OpenSSL::Cipher::CipherError, 'cipher final failed: wrong passphrase' if !plaintext.start_with?('-----BEGIN')
741
+
742
+ plaintext
379
743
  end
380
744
 
381
745
  def setup_cipher(method, passphrase, salt)