epics 2.10.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 +97 -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 +491 -123
  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 +6 -0
  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 +69 -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 +63 -0
  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 +200 -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
34
152
  end
35
153
 
36
- def e
37
- keys["E002"]
154
+ def signature_key
155
+ keyring.user_signature&.key
38
156
  end
39
157
 
40
- def a
41
- keys["A006"]
158
+ def authentication_version
159
+ keyring.user_authentication&.version
42
160
  end
43
161
 
44
- def x
45
- keys["X002"]
162
+ def authentication_key
163
+ keyring.user_authentication&.key
46
164
  end
47
165
 
48
- def bank_e
49
- keys["#{host_id.upcase}.E002"]
166
+ def bank_encryption_key
167
+ keyring.bank_encryption&.key
50
168
  end
51
169
 
52
- def bank_x
53
- keys["#{host_id.upcase}.X002"]
170
+ def bank_encryption_version
171
+ keyring.bank_encryption&.version
172
+ end
173
+
174
+ def bank_authentication_key
175
+ keyring.bank_authentication&.key
176
+ end
177
+
178
+ def bank_authentication_version
179
+ keyring.bank_authentication&.version
180
+ end
181
+
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,63 +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)
363
+ end
364
+
365
+ def CD1(document, **options)
366
+ upload(Epics::CD1, document, **options)
142
367
  end
143
368
 
144
- def AZV(document)
145
- upload(Epics::AZV, document)
369
+ def CDB(document, **options)
370
+ upload(Epics::CDB, document, **options)
146
371
  end
147
372
 
148
- def CD1(document)
149
- upload(Epics::CD1, document)
373
+ def C2S(document, **options)
374
+ upload(Epics::C2S, document, **options)
150
375
  end
151
376
 
152
- def CDB(document)
153
- upload(Epics::CDB, document)
377
+ def CDD(document, **options)
378
+ upload(Epics::CDD, document, **options)
154
379
  end
155
380
 
156
- def C2S(document)
157
- upload(Epics::C2S, document)
381
+ def XE2(document, **options)
382
+ upload(Epics::XE2, document, **options)
158
383
  end
159
384
 
160
- def CDD(document)
161
- upload(Epics::CDD, document)
385
+ def XE3(document, **options)
386
+ upload(Epics::XE3, document, **options)
162
387
  end
163
388
 
164
- def XE2(document)
165
- upload(Epics::XE2, document)
389
+ def CDS(document, **options)
390
+ upload(Epics::CDS, document, **options)
166
391
  end
167
392
 
168
- def XE3(document)
169
- upload(Epics::XE3, document)
393
+ def XDS(document, **options)
394
+ upload(Epics::XDS, document, **options)
170
395
  end
171
396
 
172
- def CDS(document)
173
- upload(Epics::CDS, document)
397
+ def CCT(document, **options)
398
+ upload(Epics::CCT, document, **options)
174
399
  end
175
400
 
176
- def XDS(document)
177
- upload(Epics::XDS, document)
401
+ def CIP(document, **options)
402
+ upload(Epics::CIP, document, **options)
178
403
  end
179
404
 
180
- def CCT(document)
181
- upload(Epics::CCT, document)
405
+ def CCS(document, **options)
406
+ upload(Epics::CCS, document, **options)
182
407
  end
183
408
 
184
- def CIP(document)
185
- upload(Epics::CIP, document)
409
+ def XCT(document, **options)
410
+ upload(Epics::XCT, document, **options)
186
411
  end
187
412
 
188
- def CCS(document)
189
- upload(Epics::CCS, document)
413
+ def YCT(document, **options)
414
+ upload(Epics::YCT, document, **options)
190
415
  end
191
416
 
192
- def XCT(document)
193
- upload(Epics::XCT, document)
417
+ def FUL(document, **options)
418
+ upload(Epics::FUL, document, **options)
194
419
  end
195
420
 
196
421
  def STA(from = nil, to = nil)
@@ -198,7 +423,7 @@ class Epics::Client
198
423
  end
199
424
 
200
425
  def FDL(format, from = nil, to = nil)
201
- download(Epics::FDL, file_format: format, from: from, to: to )
426
+ download(Epics::FDL, file_format: format, from: from, to: to)
202
427
  end
203
428
 
204
429
  def VMK(from = nil, to = nil)
@@ -217,48 +442,109 @@ class Epics::Client
217
442
  download_and_unzip(Epics::BKA, from: from, to: to)
218
443
  end
219
444
 
220
- def C52(from, to)
221
- 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)
222
448
  end
223
449
 
224
- def C53(from, to)
225
- 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)
226
453
  end
227
454
 
228
- def C54(from, to)
229
- 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)
230
458
  end
231
459
 
232
460
  def C5N(from, to)
233
461
  download_and_unzip(Epics::C5N, from: from, to: to)
234
462
  end
235
463
 
236
- def Z01(from, to)
237
- 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)
238
467
  end
239
468
 
240
- def Z52(from, to)
241
- 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)
242
472
  end
243
473
 
244
- def Z53(from, to)
245
- 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)
246
477
  end
247
478
 
248
- def Z54(from, to)
249
- 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)
250
492
  end
251
493
 
252
494
  def HAA
253
- 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
254
512
  end
255
513
 
256
514
  def HTD
257
515
  Nokogiri::XML(download(Epics::HTD)).tap do |htd|
258
- @iban ||= htd.at_xpath("//xmlns:AccountNumber[@international='true']", xmlns: "urn:org:ebics:H004").text rescue nil
259
- @bic ||= htd.at_xpath("//xmlns:BankCode[@international='true']", xmlns: "urn:org:ebics:H004").text rescue nil
260
- @name ||= htd.at_xpath("//xmlns:Name", xmlns: "urn:org:ebics:H004").text rescue nil
261
- @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
262
548
  end.to_xml
263
549
  end
264
550
 
@@ -285,32 +571,33 @@ class Epics::Client
285
571
  def save_keys(path)
286
572
  File.write(path, dump_keys)
287
573
  end
288
-
289
- def x_509_certificate(type)
290
- content = x_509_certificates_content[type.to_sym]
291
- return if content.nil? || content.empty?
292
- Epics::X509Certificate.new(content)
293
- end
294
-
295
- def x_509_certificate_hash(type)
296
- content = x_509_certificates_content[type.to_sym]
297
- return if content.nil? || content.empty?
298
- cert = OpenSSL::X509::Certificate.new(content)
299
- Digest::SHA256.hexdigest(cert.to_der).upcase
300
- end
301
574
 
302
575
  private
303
576
 
304
- def upload(order_type, document)
305
- order = order_type.new(self, document)
306
- res = post(url, order.to_xml).body
307
- 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
308
592
 
309
- 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
310
597
 
311
598
  res = post(url, order.to_transfer_xml).body
312
599
 
313
- 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? }]
314
601
  end
315
602
 
316
603
  def download(order_type, *args, **options)
@@ -318,9 +605,7 @@ class Epics::Client
318
605
  res = post(url, document.to_xml).body
319
606
  document.transaction_id = res.transaction_id
320
607
 
321
- if res.segmented? && res.last_segment?
322
- post(url, document.to_receipt_xml).body
323
- end
608
+ post(url, document.to_receipt_xml).body if res.segmented? && res.last_segment?
324
609
 
325
610
  res.order_data
326
611
  end
@@ -334,22 +619,99 @@ class Epics::Client
334
619
  end
335
620
 
336
621
  def connection
337
- @connection ||= Faraday.new(headers: { 'Content-Type' => 'text/xml', user_agent: "EPICS v#{Epics::VERSION}"}, ssl: { verify: verify_ssl? }) do |faraday|
338
- faraday.use Epics::XMLSIG, { client: self }
339
- 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 }
340
625
  # faraday.use MyAdapter
341
626
  faraday.response :logger, ::Logger.new(STDOUT), bodies: true if debug_mode # log requests/response to STDOUT
342
627
  end
343
628
  end
344
629
 
345
630
  def extract_keys
346
- JSON.load(self.keys_content).each_with_object({}) do |(type, key), memo|
347
- 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}"
348
684
  end
349
685
  end
350
686
 
351
687
  def dump_keys
352
- 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) }
353
715
  end
354
716
 
355
717
  def new_cipher
@@ -361,17 +723,23 @@ class Epics::Client
361
723
  def encrypt(data)
362
724
  salt = OpenSSL::Random.random_bytes(8)
363
725
 
364
- cipher = setup_cipher(:encrypt, self.passphrase, salt)
726
+ cipher = setup_cipher(:encrypt, passphrase, salt)
365
727
  Base64.strict_encode64([salt, cipher.update(data) + cipher.final].join)
366
728
  end
367
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.
368
733
  def decrypt(data)
369
734
  data = Base64.strict_decode64(data)
370
735
  salt = data[0..7]
371
736
  data = data[8..-1]
372
737
 
373
- cipher = setup_cipher(:decrypt, self.passphrase, salt)
374
- 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
375
743
  end
376
744
 
377
745
  def setup_cipher(method, passphrase, salt)