bts.rs (15763B)
1 /* 2 * This file is part of LibEuFin. 3 * Copyright (C) 2026 Taler Systems S.A. 4 5 * LibEuFin is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU Affero General Public License as 7 * published by the Free Software Foundation; either version 3, or 8 * (at your option) any later version. 9 10 * LibEuFin is distributed in the hope that it will be useful, but 11 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 12 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General 13 * Public License for more details. 14 15 * You should have received a copy of the GNU Affero General Public 16 * License along with LibEuFin; see the file COPYING. If not, see 17 * <http://www.gnu.org/licenses/> 18 */ 19 20 /*! EBICS protocol for business transactions */ 21 22 use compact_str::CompactString; 23 use jiff::{Timestamp, Zoned, tz::TimeZone}; 24 use roxmltree::Document; 25 use taler_common::encoding::base64; 26 27 use crate::{ 28 config::EbicsHostCfg, 29 crypto::ebics_pub_key_hash, 30 ebics::{ 31 EbicsResponse, PreparedUploadData, 32 ebics_code::EbicsReturnCode, 33 order::{BTF, Order}, 34 }, 35 keys::{BankKeys, ClientKeys}, 36 xml, 37 xml::{Xml, XmlAccess, XmlWriter}, 38 xml_sign::sign_ebics, 39 }; 40 41 fn signed_request( 42 order: &Order, 43 client: &ClientKeys, 44 lambda: impl FnOnce(&mut XmlWriter), 45 ) -> String { 46 let schema = order.schema(); 47 let doc = xml!( 48 "ebicsRequest" 49 "xmlns"=(format_args!("urn:org:ebics:{schema}")) 50 "xmlns:ds"="http://www.w3.org/2000/09/xmldsig#" 51 "Version"=schema 52 "Revision"="1" 53 { 54 @ lambda 55 } 56 ); 57 sign_ebics(doc, &client.auth).expect("EBICS signature never fails") 58 } 59 60 fn bank_digest(w: &mut XmlWriter, bank: &BankKeys) { 61 xml!(w => 62 "BankPubKeyDigests" { 63 "Authentication" "Version"="X002" "Algorithm"="http://www.w3.org/2001/04/xmlenc#sha256" : base64::fmt(ebics_pub_key_hash(&bank.auth)), 64 "Encryption" "Version"="E002" "Algorithm"="http://www.w3.org/2001/04/xmlenc#sha256" : base64::fmt(ebics_pub_key_hash(&bank.enc)) 65 }, 66 "SecurityMedium": "0000" 67 ) 68 } 69 70 fn service(w: &mut XmlWriter, service: &BTF) { 71 let BTF { 72 service: name, 73 scope, 74 msg, 75 version, 76 container, 77 option, 78 } = service; 79 xml!(w => 80 "Service" { 81 "ServiceName": name, 82 @ |w: &mut XmlWriter| { 83 if let Some(scope) = scope { 84 xml!(w => "Scope": scope) 85 } 86 if let Some(option) = option { 87 xml!(w => "ServiceOption": option) 88 } 89 if let Some(container) = container { 90 xml!(w => "Container" "containerType"=container) 91 } 92 93 if let Some(version) = version { 94 xml!(w => "MsgName" "version"=version : msg) 95 } else { 96 xml!(w => "MsgName": msg) 97 } 98 } 99 } 100 ) 101 } 102 103 pub fn d_init( 104 cfg: &EbicsHostCfg, 105 bank: &BankKeys, 106 client: &ClientKeys, 107 order: &Order, 108 range: &Option<(Timestamp, Timestamp)>, 109 ) -> String { 110 let nonce: u128 = rand::random(); 111 signed_request(order, client, |w| { 112 xml!(w => 113 "header" "authenticate"="true" { 114 "static" { 115 "HostID": cfg.host_id, 116 "Nonce": format_args!("{:032x}", nonce), 117 "Timestamp": jiff::Timestamp::now(), 118 "PartnerID": cfg.partner_id, 119 "UserID": cfg.user_id, 120 "OrderDetails" { 121 "AdminOrderType": order.ty(), 122 @ |w: &mut XmlWriter| if let Order::BTD(s) = order { 123 xml!(w => "BTDOrderParams" { 124 @ |w: &mut XmlWriter| { 125 service(w, s); 126 if let Some((start, end)) = range { 127 xml!(w => 128 "DateRange" { 129 "Start": Zoned::new(*start, TimeZone::UTC).date(), 130 "End": Zoned::new(*end, TimeZone::UTC).date() 131 } 132 ) 133 } 134 } 135 }) 136 } else { 137 xml!(w => "StandardOrderParams") 138 } 139 }, 140 @ |w: &mut XmlWriter| bank_digest(w, bank) 141 }, 142 "mutable" { 143 "TransactionPhase": "Initialisation" 144 } 145 }, 146 "AuthSignature", 147 "body" 148 ) 149 }) 150 } 151 152 pub fn d_transfer( 153 cfg: &EbicsHostCfg, 154 client: &ClientKeys, 155 order: &Order, 156 nb_segment: usize, 157 segment_nb: usize, 158 tx_id: &str, 159 ) -> String { 160 signed_request(order, client, |w| { 161 xml!(w => 162 "header" "authenticate"="true" { 163 "static" { 164 "HostID": cfg.host_id, 165 "TransactionID": tx_id 166 }, 167 "mutable" { 168 "TransactionPhase": "Transfer", 169 "SegmentNumber" "lastSegment"=(nb_segment == segment_nb) : segment_nb 170 } 171 }, 172 "AuthSignature", 173 "body" 174 ) 175 }) 176 } 177 178 pub fn receipt( 179 cfg: &EbicsHostCfg, 180 client: &ClientKeys, 181 order: &Order, 182 tx_id: &str, 183 success: bool, 184 ) -> String { 185 signed_request(order, client, |w| { 186 xml!(w => 187 "header" "authenticate"="true" { 188 "static" { 189 "HostID": cfg.host_id, 190 "TransactionID": tx_id 191 }, 192 "mutable" { 193 "TransactionPhase": "Receipt" 194 } 195 }, 196 "AuthSignature", 197 "body" { 198 "TransferReceipt" "authenticate"="true" { 199 "ReceiptCode": (if success { "0" } else { "1"}) 200 } 201 } 202 ) 203 }) 204 } 205 206 pub fn u_init( 207 cfg: &EbicsHostCfg, 208 bank: &BankKeys, 209 client: &ClientKeys, 210 order: &Order, 211 data: &PreparedUploadData, 212 ) -> String { 213 let nonce: u128 = rand::random(); 214 signed_request(order, client, |w| { 215 xml!(w => 216 "header" "authenticate"="true" { 217 "static" { 218 "HostID": cfg.host_id, 219 "Nonce": format_args!("{:032x}", nonce), 220 "Timestamp": jiff::Timestamp::now(), 221 "PartnerID": cfg.partner_id, 222 "UserID": cfg.user_id, 223 "OrderDetails" { 224 "AdminOrderType": order.ty(), 225 @ |w: &mut XmlWriter| if let Order::BTU(s) = order { 226 xml!(w => "BTUOrderParams" { 227 @ |w: &mut XmlWriter| service(w, s), 228 "SignatureFlag" 229 }) 230 } else { 231 xml!(w => "StandardOrderParams") 232 } 233 }, 234 @ |w: &mut XmlWriter| bank_digest(w, bank), 235 "NumSegments": data.nb_segments() 236 }, 237 "mutable" { 238 "TransactionPhase": "Initialisation" 239 } 240 }, 241 "AuthSignature", 242 "body" { 243 "DataTransfer" { 244 "DataEncryptionInfo" "authenticate"="true" { 245 "EncryptionPubKeyDigest" "Version"="E002" "Algorithm"="http://www.w3.org/2001/04/xmlenc#sha256": base64::fmt(ebics_pub_key_hash(&bank.enc)), 246 "TransactionKey": base64::fmt(&data.encrypted_key) 247 }, 248 "SignatureData" "authenticate"="true" : data.signature_data, 249 "DataDigest" "SignatureVersion"="A006" : base64::fmt(data.digest) 250 } 251 } 252 ) 253 }) 254 } 255 256 pub fn u_transfer( 257 cfg: &EbicsHostCfg, 258 client: &ClientKeys, 259 order: &Order, 260 tx_id: &str, 261 data: &PreparedUploadData, 262 segment_nb: usize, 263 ) -> String { 264 signed_request(order, client, |w| { 265 xml!(w => 266 "header" "authenticate"="true" { 267 "static" { 268 "HostID": cfg.host_id, 269 "TransactionID": tx_id 270 }, 271 "mutable" { 272 "TransactionPhase": "Transfer", 273 "SegmentNumber" "lastSegment"=(data.nb_segments() == segment_nb) : segment_nb 274 } 275 }, 276 "AuthSignature", 277 "body" { 278 "DataTransfer" { 279 "OrderData": data.segment(segment_nb) 280 } 281 } 282 ) 283 }) 284 } 285 286 pub struct DataEncryptionInfo { 287 pub tx_key: Vec<u8>, 288 pub enc_pub_digest: Vec<u8>, 289 } 290 291 fn expect_phase(n: Xml<'_>, phase: &str) -> xml::Result<()> { 292 let n = n.one("TransactionPhase")?; 293 if n.text() != phase { 294 Err(n.parse_err(format_args!("Expected phase '{phase}' got '{}'", n.text()))) 295 } else { 296 Ok(()) 297 } 298 } 299 300 pub struct DInit { 301 pub tx_id: CompactString, 302 pub data_encryption_info: DataEncryptionInfo, 303 pub segment: Vec<u8>, 304 pub nb_segments: usize, 305 } 306 307 pub fn parse_d_init(xml: Document) -> xml::Result<EbicsResponse<DInit>> { 308 Xml::doc(xml, "ebicsResponse", |root| { 309 let header = root.one_signed("header")?; 310 let st = header.one("static")?; 311 let mutable = header.one("mutable")?; 312 let body = root.one("body")?; 313 314 let bank_code: EbicsReturnCode = body.one_signed("ReturnCode").parse()?; 315 let technical_code: EbicsReturnCode = mutable.one("ReturnCode").parse()?; 316 let technical_text = mutable.one("ReportText").parse()?; 317 318 if technical_code.is_error() || bank_code.is_error() { 319 return Ok(EbicsResponse { 320 technical_code, 321 bank_code, 322 technical_text, 323 content: None, 324 }); 325 } 326 327 expect_phase(mutable, "Initialisation")?; 328 329 let data: Xml<'_> = body.one("DataTransfer")?; 330 let enc_info = data.one_signed("DataEncryptionInfo")?; 331 Ok(EbicsResponse { 332 technical_code, 333 bank_code, 334 technical_text, 335 content: Some(DInit { 336 tx_id: st.one("TransactionID").parse()?, 337 data_encryption_info: DataEncryptionInfo { 338 tx_key: enc_info.one("TransactionKey").b64()?, 339 enc_pub_digest: enc_info.one("EncryptionPubKeyDigest").b64()?, 340 }, 341 segment: data.one("OrderData").b64()?, 342 nb_segments: st.one("NumSegments").parse()?, 343 }), 344 }) 345 }) 346 } 347 348 pub struct DTransfer { 349 pub tx_id: CompactString, 350 pub segment: Vec<u8>, 351 } 352 353 pub fn parse_d_transfer(xml: Document) -> xml::Result<EbicsResponse<DTransfer>> { 354 Xml::doc(xml, "ebicsResponse", |root| { 355 let header = root.one_signed("header")?; 356 let st = header.one("static")?; 357 let mutable = header.one("mutable")?; 358 let body = root.one("body")?; 359 360 let bank_code: EbicsReturnCode = body.one_signed("ReturnCode").parse()?; 361 let technical_code: EbicsReturnCode = mutable.one("ReturnCode").parse()?; 362 let technical_text = mutable.one("ReportText").parse()?; 363 364 if technical_code.is_error() || bank_code.is_error() { 365 return Ok(EbicsResponse { 366 technical_code, 367 bank_code, 368 technical_text, 369 content: None, 370 }); 371 } 372 373 expect_phase(mutable, "Transfer")?; 374 375 Ok(EbicsResponse { 376 technical_code, 377 bank_code, 378 technical_text, 379 content: Some(DTransfer { 380 tx_id: st.one("TransactionID").parse()?, 381 segment: body.one("DataTransfer").one("OrderData").b64()?, 382 }), 383 }) 384 }) 385 } 386 387 pub struct Receipt { 388 pub tx_id: CompactString, 389 } 390 391 pub fn parse_receipt(xml: Document) -> xml::Result<EbicsResponse<Receipt>> { 392 Xml::doc(xml, "ebicsResponse", |root| { 393 let header = root.one_signed("header")?; 394 let st = header.one("static")?; 395 let mutable = header.one("mutable")?; 396 let body = root.one("body")?; 397 398 let bank_code: EbicsReturnCode = body.one_signed("ReturnCode").parse()?; 399 let technical_code: EbicsReturnCode = mutable.one("ReturnCode").parse()?; 400 let technical_text = mutable.one("ReportText").parse()?; 401 402 if technical_code.is_error() || bank_code.is_error() { 403 return Ok(EbicsResponse { 404 technical_code, 405 bank_code, 406 technical_text, 407 content: None, 408 }); 409 } 410 411 expect_phase(mutable, "Receipt")?; 412 413 Ok(EbicsResponse { 414 technical_code, 415 bank_code, 416 technical_text, 417 content: Some(Receipt { 418 tx_id: st.one("TransactionID").parse()?, 419 }), 420 }) 421 }) 422 } 423 424 pub struct UInit { 425 pub tx_id: CompactString, 426 pub order_id: CompactString, 427 } 428 429 pub fn parse_u_init(xml: Document) -> xml::Result<EbicsResponse<UInit>> { 430 Xml::doc(xml, "ebicsResponse", |root| { 431 let header = root.one_signed("header")?; 432 let st = header.one("static")?; 433 let mutable = header.one("mutable")?; 434 let body = root.one("body")?; 435 436 let bank_code: EbicsReturnCode = body.one_signed("ReturnCode").parse()?; 437 let technical_code: EbicsReturnCode = mutable.one("ReturnCode").parse()?; 438 let technical_text = mutable.one("ReportText").parse()?; 439 440 if technical_code.is_error() || bank_code.is_error() { 441 return Ok(EbicsResponse { 442 technical_code, 443 bank_code, 444 technical_text, 445 content: None, 446 }); 447 } 448 449 expect_phase(mutable, "Initialisation")?; 450 451 Ok(EbicsResponse { 452 technical_code, 453 bank_code, 454 technical_text, 455 content: Some(UInit { 456 order_id: mutable.one("OrderID").parse()?, 457 tx_id: st.one("TransactionID").parse()?, 458 }), 459 }) 460 }) 461 } 462 463 pub struct UTransfer { 464 pub tx_id: CompactString, 465 } 466 467 pub fn parse_u_transfer(xml: Document) -> xml::Result<EbicsResponse<UTransfer>> { 468 Xml::doc(xml, "ebicsResponse", |root| { 469 let header = root.one_signed("header")?; 470 let st = header.one("static")?; 471 let mutable = header.one("mutable")?; 472 let body = root.one("body")?; 473 474 let bank_code: EbicsReturnCode = body.one_signed("ReturnCode").parse()?; 475 let technical_code: EbicsReturnCode = mutable.one("ReturnCode").parse()?; 476 let technical_text = mutable.one("ReportText").parse()?; 477 478 if technical_code.is_error() || bank_code.is_error() { 479 return Ok(EbicsResponse { 480 technical_code, 481 bank_code, 482 technical_text, 483 content: None, 484 }); 485 } 486 487 expect_phase(mutable, "Transfer")?; 488 489 Ok(EbicsResponse { 490 technical_code, 491 bank_code, 492 technical_text, 493 content: Some(UTransfer { 494 tx_id: st.one("TransactionID").parse()?, 495 }), 496 }) 497 }) 498 }