From 0d62875dd2287857c5da172dcb3301062880810a Mon Sep 17 00:00:00 2001
From: Antoine A <>
Date: Mon, 11 Mar 2024 17:14:24 +0100
Subject: Support both 2.5 and 3 EBICS version for download
---
.../main/kotlin/tech/libeufin/nexus/EbicsFetch.kt | 5 +-
.../main/kotlin/tech/libeufin/nexus/EbicsSetup.kt | 2 +-
.../main/kotlin/tech/libeufin/nexus/EbicsSubmit.kt | 9 +-
.../src/main/kotlin/tech/libeufin/nexus/XMLUtil.kt | 10 +-
.../kotlin/tech/libeufin/nexus/ebics/Ebics2.kt | 216 ------------
.../kotlin/tech/libeufin/nexus/ebics/Ebics3.kt | 369 ---------------------
.../kotlin/tech/libeufin/nexus/ebics/EbicsBTS.kt | 347 +++++++++++++++++++
.../tech/libeufin/nexus/ebics/EbicsCommon.kt | 90 ++---
.../tech/libeufin/nexus/ebics/EbicsDialect.kt | 49 ---
.../tech/libeufin/nexus/ebics/EbicsKeyMng.kt | 212 ++++++++++++
.../kotlin/tech/libeufin/nexus/ebics/EbicsOrder.kt | 59 ++++
11 files changed, 655 insertions(+), 713 deletions(-)
delete mode 100644 nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics2.kt
delete mode 100644 nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics3.kt
create mode 100644 nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsBTS.kt
delete mode 100644 nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsDialect.kt
create mode 100644 nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsKeyMng.kt
create mode 100644 nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsOrder.kt
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt
index c74ea2f1..ca7b9116 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt
@@ -285,14 +285,13 @@ private suspend fun fetchDocuments(
}
// downloading the content
val doc = doc.doc()
- val (orderType, service) = downloadDocService(doc)
+ val order = downloadDocService(doc, doc == SupportedDocument.PAIN_002_LOGS)
ebicsDownload(
ctx.httpClient,
ctx.cfg,
ctx.clientKeys,
ctx.bankKeys,
- orderType,
- service,
+ order,
lastExecutionTime,
null
) { stream ->
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt
index 230b2cc9..b0cbc299 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt
@@ -116,7 +116,7 @@ suspend fun doKeysRequestAndUpdateState(
KeysOrderType.HIA -> impl.HIA()
KeysOrderType.HPB -> impl.HPB()
}
- val xml = client.postToBank(cfg.hostBaseUrl, req)
+ val xml = client.postToBank(cfg.hostBaseUrl, req, "$orderType")
val resp = Ebics3KeyMng.parseResponse(xml, privs.encryption_private_key)
when (orderType) {
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSubmit.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSubmit.kt
index ea20967e..82c1a459 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSubmit.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSubmit.kt
@@ -25,7 +25,7 @@ import com.github.ajalt.clikt.parameters.options.*
import io.ktor.client.*
import kotlinx.coroutines.*
import tech.libeufin.common.*
-import tech.libeufin.nexus.ebics.submitPain001
+import tech.libeufin.nexus.ebics.*
import java.time.*
import java.util.*
@@ -85,12 +85,13 @@ private suspend fun submitInitiatedPayment(
wireTransferSubject = payment.wireTransferSubject
)
ctx.fileLogger.logSubmit(xml)
- return submitPain001(
- xml,
+ return doEbicsUpload(
+ ctx.httpClient,
ctx.cfg,
ctx.clientPrivateKeysFile,
ctx.bankPublicKeysFile,
- ctx.httpClient
+ uploadPaymentService(),
+ xml
)
}
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/XMLUtil.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/XMLUtil.kt
index bbae68ae..2893f367 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/XMLUtil.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/XMLUtil.kt
@@ -113,11 +113,10 @@ object XMLUtil {
fun signEbicsDocument(
doc: Document,
signingPriv: PrivateKey,
- withEbics3: Boolean = false
+ schema: String
) {
- val ns = if (withEbics3) "urn:org:ebics:H005" else "urn:org:ebics:H004"
val authSigNode = XPathFactory.newInstance().newXPath()
- .evaluate("/*[1]/$ns:AuthSignature", doc, XPathConstants.NODE)
+ .evaluate("/*[1]/urn:org:ebics:$schema:AuthSignature", doc, XPathConstants.NODE)
if (authSigNode !is Node)
throw java.lang.Exception("no AuthSignature")
val fac = XMLSignatureFactory.getInstance("DOM")
@@ -150,12 +149,11 @@ object XMLUtil {
fun verifyEbicsDocument(
doc: Document,
signingPub: PublicKey,
- withEbics3: Boolean = false
+ schema: String
): Boolean {
val doc2: Document = doc.cloneNode(true) as Document
- val ns = if (withEbics3) "urn:org:ebics:H005" else "urn:org:ebics:H004"
val authSigNode = XPathFactory.newInstance().newXPath()
- .evaluate("/*[1]/$ns:AuthSignature", doc2, XPathConstants.NODE)
+ .evaluate("/*[1]/urn:org:ebics:$schema:AuthSignature", doc2, XPathConstants.NODE)
if (authSigNode !is Node)
throw java.lang.Exception("no AuthSignature")
val sigEl = doc2.createElementNS("http://www.w3.org/2000/09/xmldsig#", "ds:Signature")
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics2.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics2.kt
deleted file mode 100644
index a7c2ba50..00000000
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics2.kt
+++ /dev/null
@@ -1,216 +0,0 @@
-/*
- * This file is part of LibEuFin.
- * Copyright (C) 2024 Taler Systems S.A.
-
- * LibEuFin is free software; you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation; either version 3, or
- * (at your option) any later version.
-
- * LibEuFin is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
- * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
- * Public License for more details.
-
- * You should have received a copy of the GNU Affero General Public
- * License along with LibEuFin; see the file COPYING. If not, see
- *
- */
-
-/**
- * This file contains helpers to construct EBICS 2.x requests.
- */
-
-package tech.libeufin.nexus.ebics
-
-import org.w3c.dom.Document
-import tech.libeufin.common.crypto.CryptoUtil
-import tech.libeufin.common.*
-import tech.libeufin.nexus.*
-import tech.libeufin.nexus.BankPublicKeysFile
-import tech.libeufin.nexus.ClientPrivateKeysFile
-import tech.libeufin.nexus.EbicsSetupConfig
-import java.io.InputStream
-import java.time.Instant
-import java.time.ZoneId
-import java.util.*
-import javax.xml.datatype.DatatypeFactory
-import java.security.interfaces.*
-
-/** Ebics 3 protocol for */
-class Ebics3KeyMng(
- private val cfg: EbicsSetupConfig,
- private val clientKeys: ClientPrivateKeysFile
-) {
- fun INI(): ByteArray {
- val inner = XMLOrderData(cfg, "ns2:SignaturePubKeyOrderData", "http://www.ebics.org/S001") {
- el("ns2:SignaturePubKeyInfo") {
- RSAKeyXml(clientKeys.signature_private_key)
- el("ns2:SignatureVersion", "A006")
- }
- }
- val doc = XmlBuilder.toDom("ebicsUnsecuredRequest", "urn:org:ebics:H004") {
- attr("http://www.w3.org/2000/xmlns/", "xmlns", "urn:org:ebics:H004")
- attr("http://www.w3.org/2000/xmlns/", "xmlns:ds", "http://www.w3.org/2000/09/xmldsig#")
- attr("Version", "H004")
- attr("Revision", "1")
- el("header") {
- attr("authenticate", "true")
- el("static") {
- el("HostID", cfg.ebicsHostId)
- el("PartnerID", cfg.ebicsPartnerId)
- el("UserID", cfg.ebicsUserId)
- el("OrderDetails") {
- el("OrderType", "INI")
- el("OrderAttribute", "DZNNN")
- }
- el("SecurityMedium", "0200")
- }
- el("mutable")
- }
- el("body/DataTransfer/OrderData", inner)
- }
- return XMLUtil.convertDomToBytes(doc)
- }
-
- fun HIA(): ByteArray {
- val inner = XMLOrderData(cfg, "ns2:HIARequestOrderData", "urn:org:ebics:H004") {
- el("ns2:AuthenticationPubKeyInfo") {
- RSAKeyXml(clientKeys.authentication_private_key)
- el("ns2:AuthenticationVersion", "X002")
- }
- el("ns2:EncryptionPubKeyInfo") {
- RSAKeyXml(clientKeys.encryption_private_key)
- el("ns2:EncryptionVersion", "E002")
- }
- }
- val doc = XmlBuilder.toDom("ebicsUnsecuredRequest", "urn:org:ebics:H004") {
- attr("http://www.w3.org/2000/xmlns/", "xmlns", "urn:org:ebics:H004")
- attr("http://www.w3.org/2000/xmlns/", "xmlns:ds", "http://www.w3.org/2000/09/xmldsig#")
- attr("Version", "H004")
- attr("Revision", "1")
- el("header") {
- attr("authenticate", "true")
- el("static") {
- el("HostID", cfg.ebicsHostId)
- el("PartnerID", cfg.ebicsPartnerId)
- el("UserID", cfg.ebicsUserId)
- el("OrderDetails") {
- el("OrderType", "HIA")
- el("OrderAttribute", "DZNNN")
- }
- el("SecurityMedium", "0200")
- }
- el("mutable")
- }
- el("body/DataTransfer/OrderData", inner)
- }
- return XMLUtil.convertDomToBytes(doc)
- }
-
- fun HPB(): ByteArray {
- val nonce = getNonce(128)
- val doc = XmlBuilder.toDom("ebicsNoPubKeyDigestsRequest", "urn:org:ebics:H004") {
- attr("http://www.w3.org/2000/xmlns/", "xmlns", "urn:org:ebics:H004")
- attr("http://www.w3.org/2000/xmlns/", "xmlns:ds", "http://www.w3.org/2000/09/xmldsig#")
- attr("Version", "H004")
- attr("Revision", "1")
- el("header") {
- attr("authenticate", "true")
- el("static") {
- el("HostID", cfg.ebicsHostId)
- el("Nonce", nonce.encodeUpHex())
- el("Timestamp", Instant.now().xmlDateTime())
- el("PartnerID", cfg.ebicsPartnerId)
- el("UserID", cfg.ebicsUserId)
- el("OrderDetails") {
- el("OrderType", "HPB")
- el("OrderAttribute", "DZHNN")
- }
- el("SecurityMedium", "0000")
- }
- el("mutable")
- }
- el("AuthSignature")
- el("body")
- }
- XMLUtil.signEbicsDocument(doc, clientKeys.authentication_private_key)
- return XMLUtil.convertDomToBytes(doc)
- }
-
- /* ----- Helpers ----- */
-
- private fun XmlBuilder.RSAKeyXml(key: RSAPrivateCrtKey) {
- el("ns2:PubKeyValue") {
- el("ds:RSAKeyValue") {
- el("ds:Modulus", key.modulus.encodeBase64())
- el("ds:Exponent", key.publicExponent.encodeBase64())
- }
- }
- }
-
- private fun XMLOrderData(cfg: EbicsSetupConfig, name: String, schema: String, build: XmlBuilder.() -> Unit): String {
- return XmlBuilder.toBytes(name) {
- attr("xmlns:ds", "http://www.w3.org/2000/09/xmldsig#")
- attr("xmlns:ns2", schema)
- build()
- el("ns2:PartnerID", cfg.ebicsPartnerId)
- el("ns2:UserID", cfg.ebicsUserId)
- }.inputStream().deflate().encodeBase64()
- }
-
- companion object {
- fun parseResponse(doc: Document, clientEncryptionKey: RSAPrivateCrtKey): EbicsResponse {
- return XmlDestructor.fromDoc(doc, "ebicsKeyManagementResponse") {
- lateinit var technicalCode: EbicsReturnCode
- lateinit var bankCode: EbicsReturnCode
- var payload: InputStream? = null
- one("header") {
- one("mutable") {
- technicalCode = EbicsReturnCode.lookup(one("ReturnCode").text())
- }
- }
- one("body") {
- bankCode = EbicsReturnCode.lookup(one("ReturnCode").text())
- payload = opt("DataTransfer") {
- val descriptionInfo = one("DataEncryptionInfo") {
- DataEncryptionInfo(
- one("TransactionKey").text().decodeBase64(),
- one("EncryptionPubKeyDigest").text().decodeBase64()
- )
- }
- val chunk = one("OrderData").text().decodeBase64()
- decryptAndDecompressPayload(
- clientEncryptionKey,
- descriptionInfo,
- listOf(chunk)
- )
- }
- }
- EbicsResponse(
- technicalCode = technicalCode,
- bankCode,
- content = payload
- )
- }
- }
-
- fun parseHpbOrder(data: InputStream): Pair {
- return XmlDestructor.fromStream(data, "HPBResponseOrderData") {
- val authPub = one("AuthenticationPubKeyInfo").one("PubKeyValue").one("RSAKeyValue") {
- CryptoUtil.loadRsaPublicKeyFromComponents(
- one("Modulus").text().decodeBase64(),
- one("Exponent").text().decodeBase64(),
- )
- }
- val encPub = one("EncryptionPubKeyInfo").one("PubKeyValue").one("RSAKeyValue") {
- CryptoUtil.loadRsaPublicKeyFromComponents(
- one("Modulus").text().decodeBase64(),
- one("Exponent").text().decodeBase64(),
- )
- }
- Pair(authPub, encPub)
- }
- }
- }
-}
\ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics3.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics3.kt
deleted file mode 100644
index 5acba13b..00000000
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics3.kt
+++ /dev/null
@@ -1,369 +0,0 @@
-/*
- * This file is part of LibEuFin.
- * Copyright (C) 2024 Taler Systems S.A.
-
- * LibEuFin is free software; you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation; either version 3, or
- * (at your option) any later version.
-
- * LibEuFin is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
- * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
- * Public License for more details.
-
- * You should have received a copy of the GNU Affero General Public
- * License along with LibEuFin; see the file COPYING. If not, see
- *
- */
-package tech.libeufin.nexus.ebics
-
-import io.ktor.client.*
-import tech.libeufin.nexus.*
-import tech.libeufin.common.*
-import tech.libeufin.common.crypto.*
-import java.math.BigInteger
-import java.time.*
-import java.time.format.*
-import java.util.*
-import java.io.File
-import org.w3c.dom.*
-import javax.xml.datatype.XMLGregorianCalendar
-import javax.xml.datatype.DatatypeFactory
-import java.security.interfaces.*
-
-
-fun Instant.xmlDate(): String = DateTimeFormatter.ISO_DATE.withZone(ZoneId.of("UTC")).format(this)
-fun Instant.xmlDateTime(): String = DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(ZoneId.of("UTC")).format(this)
-
-// TODO WIP
-fun iniRequest(
- cfg: EbicsSetupConfig,
- clientKeys: ClientPrivateKeysFile
-): ByteArray {
- val temp = XmlBuilder.toBytes("ns2:SignaturePubKeyOrderData") {
- attr("xmlns:ds", "http://www.w3.org/2000/09/xmldsig#")
- attr("xmlns:ns2", "http://www.ebics.org/S001")
- el("ns2:SignaturePubKeyInfo") {
- el("ns2:PubKeyValue") {
- el("ds:RSAKeyValue") {
- el("ds:Modulus", clientKeys.signature_private_key.modulus.encodeBase64())
- el("ds:Exponent", clientKeys.signature_private_key.publicExponent.encodeBase64())
- }
- }
- el("ns2:SignatureVersion", "A006")
- }
- el("ns2:PartnerID", cfg.ebicsPartnerId)
- el("ns2:UserID", cfg.ebicsUserId)
- }
- // TODO in ebics:H005 we MUST use x509 certificates ...
- println(temp)
- val inner = temp.inputStream().deflate().encodeBase64()
- val doc = XmlBuilder.toDom("ebicsUnsecuredRequest", "urn:org:ebics:H005") {
- attr("http://www.w3.org/2000/xmlns/", "xmlns", "urn:org:ebics:H005")
- attr("http://www.w3.org/2000/xmlns/", "xmlns:ds", "http://www.w3.org/2000/09/xmldsig#")
- attr("Version", "H005")
- attr("Revision", "1")
- el("header") {
- attr("authenticate", "true")
- el("static") {
- el("HostID", cfg.ebicsHostId)
- el("PartnerID", cfg.ebicsPartnerId)
- el("UserID", cfg.ebicsUserId)
- el("OrderDetails/AdminOrderType", "INI")
- el("SecurityMedium", "0200")
- }
- el("mutable")
- }
- el("body/DataTransfer/OrderData", inner)
- }
- return XMLUtil.convertDomToBytes(doc)
-}
-
-/** EBICS 3 protocol for business transactions */
-class Ebics3BTS(
- private val cfg: EbicsSetupConfig,
- private val bankKeys: BankPublicKeysFile,
- private val clientKeys: ClientPrivateKeysFile
-) {
- /* ----- Download ----- */
-
- fun downloadInitialization(orderType: String, service: Ebics3Service?, startDate: Instant?, endDate: Instant?): ByteArray {
- val nonce = getNonce(128)
- return signedRequest {
- el("header") {
- attr("authenticate", "true")
- el("static") {
- el("HostID", cfg.ebicsHostId)
- el("Nonce", nonce.encodeHex())
- el("Timestamp", Instant.now().xmlDateTime())
- el("PartnerID", cfg.ebicsPartnerId)
- el("UserID", cfg.ebicsUserId)
- // SystemID
- // Product
- el("OrderDetails") {
- el("AdminOrderType", orderType)
- if (orderType == "BTD") {
- el("BTDOrderParams") {
- if (service != null) {
- el("Service") {
- el("ServiceName", service.name)
- el("Scope", service.scope)
- if (service.container != null) {
- el("Container") {
- attr("containerType", service.container)
- }
- }
- el("MsgName") {
- attr("version", service.messageVersion)
- text(service.messageName)
- }
- }
- }
- if (startDate != null) {
- el("DateRange") {
- el("Start", startDate.xmlDate())
- el("End", (endDate ?: Instant.now()).xmlDate())
- }
- }
- }
- }
- }
- bankDigest()
- }
- el("mutable/TransactionPhase", "Initialisation")
- }
- el("AuthSignature")
- el("body")
- }
- }
-
- fun downloadTransfer(
- howManySegments: Int,
- segmentNumber: Int,
- transactionId: String
- ): ByteArray {
- return signedRequest {
- el("header") {
- attr("authenticate", "true")
- el("static") {
- el("HostID", cfg.ebicsHostId)
- el("TransactionID", transactionId)
- }
- el("mutable") {
- el("TransactionPhase", "Transfer")
- el("SegmentNumber") {
- attr("lastSegment", if (howManySegments == segmentNumber) "true" else "false")
- }
- }
- }
- el("AuthSignature")
- el("body")
- }
- }
-
- fun downloadReceipt(
- transactionId: String,
- success: Boolean
- ): ByteArray {
- return signedRequest {
- el("header") {
- attr("authenticate", "true")
- el("static") {
- el("HostID", cfg.ebicsHostId)
- el("TransactionID", transactionId)
- }
- el("mutable") {
- el("TransactionPhase", "Receipt")
- }
- }
- el("AuthSignature")
- el("body/TransferReceipt") {
- attr("authenticate", "true")
- el("ReceiptCode", if (success) "0" else "1")
- }
- }
- }
-
- /* ----- Upload ----- */
-
- fun uploadInitialization(service: Ebics3Service, preparedUploadData: PreparedUploadData): ByteArray {
- val nonce = getNonce(128)
- return signedRequest {
- el("header") {
- attr("authenticate", "true")
- el("static") {
- el("HostID", cfg.ebicsHostId)
- el("Nonce", nonce.encodeUpHex())
- el("Timestamp", Instant.now().xmlDateTime())
- el("PartnerID", cfg.ebicsPartnerId)
- el("UserID", cfg.ebicsUserId)
- // SystemID
- // Product
- el("OrderDetails") {
- el("AdminOrderType", "BTU")
- el("BTUOrderParams") {
- el("Service") {
- el("ServiceName", service.name)
- el("Scope", service.scope)
- el("MsgName") {
- attr("version", service.messageVersion)
- text(service.messageName)
- }
- }
- el("SignatureFlag", "true")
- }
- }
- bankDigest()
- el("NumSegments", "1") // TODO test upload of many segment
-
- }
- el("mutable") {
- el("TransactionPhase", "Initialisation")
- }
- }
- el("AuthSignature")
- el("body") {
- el("DataTransfer") {
- el("DataEncryptionInfo") {
- attr("authenticate", "true")
- el("EncryptionPubKeyDigest") {
- attr("Version", "E002")
- attr("Algorithm", "http://www.w3.org/2001/04/xmlenc#sha256")
- text(CryptoUtil.getEbicsPublicKeyHash(bankKeys.bank_encryption_public_key).encodeBase64())
- }
- el("TransactionKey", preparedUploadData.transactionKey.encodeBase64())
- }
- el("SignatureData") {
- attr("authenticate", "true")
- text(preparedUploadData.userSignatureDataEncrypted.encodeBase64())
- }
- el("DataDigest") {
- attr("SignatureVersion", "A006")
- text(preparedUploadData.dataDigest.encodeBase64())
- }
- }
- }
- }
- }
-
- fun uploadTransfer(
- transactionId: String,
- uploadData: PreparedUploadData
- ): ByteArray {
- val chunkIndex = 1 // TODO test upload of many segment
- return signedRequest {
- el("header") {
- attr("authenticate", "true")
- el("static") {
- el("HostID", cfg.ebicsHostId)
- el("TransactionID", transactionId)
- }
- el("mutable") {
- el("TransactionPhase", "Transfer")
- el("SegmentNumber") {
- attr("lastSegment", "true")
- text(chunkIndex.toString())
- }
- }
- }
- el("AuthSignature")
- el("body/DataTransfer/OrderData", uploadData.encryptedPayloadChunks[chunkIndex - 1])
- }
- }
-
- /* ----- Helpers ----- */
-
- /** Generate a signed H005 ebicsRequest */
- private fun signedRequest(lambda: XmlBuilder.() -> Unit): ByteArray {
- val doc = XmlBuilder.toDom("ebicsRequest", "urn:org:ebics:H005") {
- attr("http://www.w3.org/2000/xmlns/", "xmlns", "urn:org:ebics:H005")
- attr("http://www.w3.org/2000/xmlns/", "xmlns:ds", "http://www.w3.org/2000/09/xmldsig#")
- attr("Version", "H005")
- attr("Revision", "1")
- lambda()
- }
- XMLUtil.signEbicsDocument(
- doc,
- clientKeys.authentication_private_key,
- withEbics3 = true
- )
- return XMLUtil.convertDomToBytes(doc)
- }
-
- private fun XmlBuilder.bankDigest() {
- el("BankPubKeyDigests") {
- el("Authentication") {
- attr("Version", "X002")
- attr("Algorithm", "http://www.w3.org/2001/04/xmlenc#sha256")
- text(CryptoUtil.getEbicsPublicKeyHash(bankKeys.bank_authentication_public_key).encodeBase64())
- }
- el("Encryption") {
- attr("Version", "E002")
- attr("Algorithm", "http://www.w3.org/2001/04/xmlenc#sha256")
- text(CryptoUtil.getEbicsPublicKeyHash(bankKeys.bank_encryption_public_key).encodeBase64())
- }
- // Signature
- }
- el("SecurityMedium", "0000")
- }
-
- companion object {
- fun parseResponse(doc: Document): EbicsResponse {
- return XmlDestructor.fromDoc(doc, "ebicsResponse") {
- var transactionID: String? = null
- var numSegments: Int? = null
- lateinit var technicalCode: EbicsReturnCode
- lateinit var bankCode: EbicsReturnCode
- var orderID: String? = null
- var segmentNumber: Int? = null
- var payloadChunk: ByteArray? = null
- var dataEncryptionInfo: DataEncryptionInfo? = null
- one("header") {
- one("static") {
- transactionID = opt("TransactionID")?.text()
- numSegments = opt("NumSegments")?.text()?.toInt()
- }
- one("mutable") {
- segmentNumber = opt("SegmentNumber")?.text()?.toInt()
- orderID = opt("OrderID")?.text()
- technicalCode = EbicsReturnCode.lookup(one("ReturnCode").text())
- }
- }
- one("body") {
- opt("DataTransfer") {
- payloadChunk = one("OrderData").text().decodeBase64()
- dataEncryptionInfo = opt("DataEncryptionInfo") {
- DataEncryptionInfo(
- one("TransactionKey").text().decodeBase64(),
- one("EncryptionPubKeyDigest").text().decodeBase64()
- )
- }
- }
- bankCode = EbicsReturnCode.lookup(one("ReturnCode").text())
- }
- EbicsResponse(
- bankCode = bankCode,
- technicalCode = technicalCode,
- content = BTSResponse(
- transactionID = transactionID,
- orderID = orderID,
- payloadChunk = payloadChunk,
- dataEncryptionInfo = dataEncryptionInfo,
- numSegments = numSegments,
- segmentNumber = segmentNumber
- )
- )
- }
- }
- }
-}
-
-data class BTSResponse(
- val transactionID: String?,
- val orderID: String?,
- val dataEncryptionInfo: DataEncryptionInfo?,
- val payloadChunk: ByteArray?,
- val segmentNumber: Int?,
- val numSegments: Int?
-)
\ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsBTS.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsBTS.kt
new file mode 100644
index 00000000..356d4b96
--- /dev/null
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsBTS.kt
@@ -0,0 +1,347 @@
+/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 2024 Taler Systems S.A.
+
+ * LibEuFin is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3, or
+ * (at your option) any later version.
+
+ * LibEuFin is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
+ * Public License for more details.
+
+ * You should have received a copy of the GNU Affero General Public
+ * License along with LibEuFin; see the file COPYING. If not, see
+ *
+ */
+package tech.libeufin.nexus.ebics
+
+import io.ktor.client.*
+import tech.libeufin.nexus.*
+import tech.libeufin.common.*
+import tech.libeufin.common.crypto.*
+import java.math.BigInteger
+import java.time.*
+import java.time.format.*
+import java.util.*
+import java.io.File
+import org.w3c.dom.*
+import javax.xml.datatype.XMLGregorianCalendar
+import javax.xml.datatype.DatatypeFactory
+import java.security.interfaces.*
+
+
+fun Instant.xmlDate(): String = DateTimeFormatter.ISO_DATE.withZone(ZoneId.of("UTC")).format(this)
+fun Instant.xmlDateTime(): String = DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(ZoneId.of("UTC")).format(this)
+
+/** EBICS protocol for business transactions */
+class EbicsBTS(
+ val cfg: EbicsSetupConfig,
+ val bankKeys: BankPublicKeysFile,
+ val clientKeys: ClientPrivateKeysFile,
+ val order: EbicsOrder
+) {
+ /* ----- Download ----- */
+
+ fun downloadInitialization(startDate: Instant?, endDate: Instant?): ByteArray {
+ val nonce = getNonce(128)
+ return signedRequest {
+ el("header") {
+ attr("authenticate", "true")
+ el("static") {
+ el("HostID", cfg.ebicsHostId)
+ el("Nonce", nonce.encodeHex())
+ el("Timestamp", Instant.now().xmlDateTime())
+ el("PartnerID", cfg.ebicsPartnerId)
+ el("UserID", cfg.ebicsUserId)
+ // SystemID
+ // Product
+ el("OrderDetails") {
+ when (order) {
+ is EbicsOrder.V2_5 -> {
+ el("OrderType", order.type)
+ el("OrderAttribute", order.attribute)
+ el("StandardOrderParams") {
+ if (startDate != null) {
+ el("DateRange") {
+ el("Start", startDate.xmlDate())
+ el("End", (endDate ?: Instant.now()).xmlDate())
+ }
+ }
+ }
+ }
+ is EbicsOrder.V3 -> {
+ el("AdminOrderType", order.type)
+ if (order.type == "BTD") {
+ el("BTDOrderParams") {
+ el("Service") {
+ el("ServiceName", order.name!!)
+ el("Scope", order.scope!!)
+ if (order.container != null) {
+ el("Container") {
+ attr("containerType", order.container)
+ }
+ }
+ el("MsgName") {
+ attr("version", order.messageVersion!!)
+ text(order.messageName!!)
+ }
+ }
+ if (startDate != null) {
+ el("DateRange") {
+ el("Start", startDate.xmlDate())
+ el("End", (endDate ?: Instant.now()).xmlDate())
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ bankDigest()
+ }
+ el("mutable/TransactionPhase", "Initialisation")
+ }
+ el("AuthSignature")
+ el("body")
+ }
+ }
+
+ fun downloadTransfer(
+ howManySegments: Int,
+ segmentNumber: Int,
+ transactionId: String
+ ): ByteArray {
+ return signedRequest {
+ el("header") {
+ attr("authenticate", "true")
+ el("static") {
+ el("HostID", cfg.ebicsHostId)
+ el("TransactionID", transactionId)
+ }
+ el("mutable") {
+ el("TransactionPhase", "Transfer")
+ el("SegmentNumber") {
+ attr("lastSegment", if (howManySegments == segmentNumber) "true" else "false")
+ }
+ }
+ }
+ el("AuthSignature")
+ el("body")
+ }
+ }
+
+ fun downloadReceipt(
+ transactionId: String,
+ success: Boolean
+ ): ByteArray {
+ return signedRequest {
+ el("header") {
+ attr("authenticate", "true")
+ el("static") {
+ el("HostID", cfg.ebicsHostId)
+ el("TransactionID", transactionId)
+ }
+ el("mutable") {
+ el("TransactionPhase", "Receipt")
+ }
+ }
+ el("AuthSignature")
+ el("body/TransferReceipt") {
+ attr("authenticate", "true")
+ el("ReceiptCode", if (success) "0" else "1")
+ }
+ }
+ }
+
+ /* ----- Upload ----- */
+
+ fun uploadInitialization(preparedUploadData: PreparedUploadData): ByteArray {
+ val nonce = getNonce(128)
+ return signedRequest {
+ el("header") {
+ attr("authenticate", "true")
+ el("static") {
+ el("HostID", cfg.ebicsHostId)
+ el("Nonce", nonce.encodeUpHex())
+ el("Timestamp", Instant.now().xmlDateTime())
+ el("PartnerID", cfg.ebicsPartnerId)
+ el("UserID", cfg.ebicsUserId)
+ // SystemID
+ // Product
+ el("OrderDetails") {
+ when (order) {
+ is EbicsOrder.V2_5 -> {
+ // TODO
+ }
+ is EbicsOrder.V3 -> {
+ el("AdminOrderType", order.type)
+ el("BTUOrderParams") {
+ el("Service") {
+ el("ServiceName", order.name!!)
+ el("Scope", order.scope!!)
+ el("MsgName") {
+ attr("version", order.messageVersion!!)
+ text(order.messageName!!)
+ }
+ }
+ el("SignatureFlag", "true")
+ }
+ }
+ }
+ }
+ bankDigest()
+ el("NumSegments", "1") // TODO test upload of many segment
+
+ }
+ el("mutable") {
+ el("TransactionPhase", "Initialisation")
+ }
+ }
+ el("AuthSignature")
+ el("body") {
+ el("DataTransfer") {
+ el("DataEncryptionInfo") {
+ attr("authenticate", "true")
+ el("EncryptionPubKeyDigest") {
+ attr("Version", "E002")
+ attr("Algorithm", "http://www.w3.org/2001/04/xmlenc#sha256")
+ text(CryptoUtil.getEbicsPublicKeyHash(bankKeys.bank_encryption_public_key).encodeBase64())
+ }
+ el("TransactionKey", preparedUploadData.transactionKey.encodeBase64())
+ }
+ el("SignatureData") {
+ attr("authenticate", "true")
+ text(preparedUploadData.userSignatureDataEncrypted.encodeBase64())
+ }
+ el("DataDigest") {
+ attr("SignatureVersion", "A006")
+ text(preparedUploadData.dataDigest.encodeBase64())
+ }
+ }
+ }
+ }
+ }
+
+ fun uploadTransfer(
+ transactionId: String,
+ uploadData: PreparedUploadData
+ ): ByteArray {
+ val chunkIndex = 1 // TODO test upload of many segment
+ return signedRequest {
+ el("header") {
+ attr("authenticate", "true")
+ el("static") {
+ el("HostID", cfg.ebicsHostId)
+ el("TransactionID", transactionId)
+ }
+ el("mutable") {
+ el("TransactionPhase", "Transfer")
+ el("SegmentNumber") {
+ attr("lastSegment", "true")
+ text(chunkIndex.toString())
+ }
+ }
+ }
+ el("AuthSignature")
+ el("body/DataTransfer/OrderData", uploadData.encryptedPayloadChunks[chunkIndex - 1])
+ }
+ }
+
+ /* ----- Helpers ----- */
+
+ /** Generate a signed ebicsRequest */
+ private fun signedRequest(lambda: XmlBuilder.() -> Unit): ByteArray {
+ val doc = XmlBuilder.toDom("ebicsRequest", "urn:org:ebics:${order.schema}") {
+ attr("http://www.w3.org/2000/xmlns/", "xmlns", "urn:org:ebics:${order.schema}")
+ attr("http://www.w3.org/2000/xmlns/", "xmlns:ds", "http://www.w3.org/2000/09/xmldsig#")
+ attr("Version", order.schema)
+ attr("Revision", "1")
+ lambda()
+ }
+ XMLUtil.signEbicsDocument(
+ doc,
+ clientKeys.authentication_private_key,
+ order.schema
+ )
+ return XMLUtil.convertDomToBytes(doc)
+ }
+
+ private fun XmlBuilder.bankDigest() {
+ el("BankPubKeyDigests") {
+ el("Authentication") {
+ attr("Version", "X002")
+ attr("Algorithm", "http://www.w3.org/2001/04/xmlenc#sha256")
+ text(CryptoUtil.getEbicsPublicKeyHash(bankKeys.bank_authentication_public_key).encodeBase64())
+ }
+ el("Encryption") {
+ attr("Version", "E002")
+ attr("Algorithm", "http://www.w3.org/2001/04/xmlenc#sha256")
+ text(CryptoUtil.getEbicsPublicKeyHash(bankKeys.bank_encryption_public_key).encodeBase64())
+ }
+ // Signature
+ }
+ el("SecurityMedium", "0000")
+ }
+
+ companion object {
+ fun parseResponse(doc: Document): EbicsResponse {
+ return XmlDestructor.fromDoc(doc, "ebicsResponse") {
+ var transactionID: String? = null
+ var numSegments: Int? = null
+ lateinit var technicalCode: EbicsReturnCode
+ lateinit var bankCode: EbicsReturnCode
+ var orderID: String? = null
+ var segmentNumber: Int? = null
+ var payloadChunk: ByteArray? = null
+ var dataEncryptionInfo: DataEncryptionInfo? = null
+ one("header") {
+ one("static") {
+ transactionID = opt("TransactionID")?.text()
+ numSegments = opt("NumSegments")?.text()?.toInt()
+ }
+ one("mutable") {
+ segmentNumber = opt("SegmentNumber")?.text()?.toInt()
+ orderID = opt("OrderID")?.text()
+ technicalCode = EbicsReturnCode.lookup(one("ReturnCode").text())
+ }
+ }
+ one("body") {
+ opt("DataTransfer") {
+ payloadChunk = one("OrderData").text().decodeBase64()
+ dataEncryptionInfo = opt("DataEncryptionInfo") {
+ DataEncryptionInfo(
+ one("TransactionKey").text().decodeBase64(),
+ one("EncryptionPubKeyDigest").text().decodeBase64()
+ )
+ }
+ }
+ bankCode = EbicsReturnCode.lookup(one("ReturnCode").text())
+ }
+ EbicsResponse(
+ bankCode = bankCode,
+ technicalCode = technicalCode,
+ content = BTSResponse(
+ transactionID = transactionID,
+ orderID = orderID,
+ payloadChunk = payloadChunk,
+ dataEncryptionInfo = dataEncryptionInfo,
+ numSegments = numSegments,
+ segmentNumber = segmentNumber
+ )
+ )
+ }
+ }
+ }
+}
+
+data class BTSResponse(
+ val transactionID: String?,
+ val orderID: String?,
+ val dataEncryptionInfo: DataEncryptionInfo?,
+ val payloadChunk: ByteArray?,
+ val segmentNumber: Int?,
+ val numSegments: Int?
+)
\ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt
index 69e6da19..74c1dd32 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt
@@ -109,45 +109,45 @@ sealed class EbicsError(msg: String, cause: Throwable? = null): Exception(msg, c
* @param msg EBICS message as raw bytes.
* @return the raw bank response.
*/
-suspend fun HttpClient.postToBank(bankUrl: String, msg: ByteArray): Document {
+suspend fun HttpClient.postToBank(bankUrl: String, msg: ByteArray, phase: String): Document {
val res = try {
post(urlString = bankUrl) {
contentType(ContentType.Text.Xml)
setBody(msg)
}
} catch (e: Exception) {
- throw EbicsError.Transport("failed to contact bank", e)
+ throw EbicsError.Transport("$phase: failed to contact bank", e)
}
if (res.status != HttpStatusCode.OK) {
- throw EbicsError.Transport("bank HTTP error: ${res.status}")
+ throw EbicsError.Transport("$phase: bank HTTP error: ${res.status}")
}
try {
return XMLUtil.parseIntoDom(res.bodyAsChannel().toInputStream())
} catch (e: SAXException) {
- throw EbicsError.Protocol("invalid XML bank reponse", e)
+ throw EbicsError.Protocol("$phase: invalid XML bank reponse", e)
} catch (e: Exception) {
- throw EbicsError.Transport("failed read bank response", e)
+ throw EbicsError.Transport("$phase: failed read bank response", e)
}
}
-suspend fun postBTS(
+
+suspend fun EbicsBTS.postBTS(
client: HttpClient,
- cfg: EbicsSetupConfig,
- bankKeys: BankPublicKeysFile,
- xmlReq: ByteArray
+ xmlReq: ByteArray,
+ phase: String,
): EbicsResponse {
- val doc = client.postToBank(cfg.hostBaseUrl, xmlReq)
+ val doc = client.postToBank(cfg.hostBaseUrl, xmlReq, phase)
if (!XMLUtil.verifyEbicsDocument(
doc,
bankKeys.bank_authentication_public_key,
- true
+ order.schema
)) {
- throw EbicsError.Protocol("bank signature did not verify")
+ throw EbicsError.Protocol("$phase: bank signature did not verify")
}
try {
- return Ebics3BTS.parseResponse(doc)
+ return EbicsBTS.parseResponse(doc)
} catch (e: Exception) {
- throw EbicsError.Protocol("invalid ebics response", e)
+ throw EbicsError.Protocol("$phase: invalid ebics response", e)
}
}
@@ -170,13 +170,12 @@ suspend fun ebicsDownload(
cfg: EbicsSetupConfig,
clientKeys: ClientPrivateKeysFile,
bankKeys: BankPublicKeysFile,
- orderType: String,
- service: Ebics3Service?,
+ order: EbicsOrder,
startDate: Instant?,
endDate: Instant?,
processing: (InputStream) -> Unit,
) = coroutineScope {
- val impl = Ebics3BTS(cfg, bankKeys, clientKeys)
+ val impl = EbicsBTS(cfg, bankKeys, clientKeys, order)
val parentScope = this
// We need to run the logic in a non-cancelable context because we need to send
@@ -185,8 +184,8 @@ suspend fun ebicsDownload(
// TODO find a way to cancel the pending transaction ?
withContext(NonCancellable) {
// Init phase
- val initReq = impl.downloadInitialization(orderType, service, startDate, endDate)
- val initResp = postBTS(client, cfg, bankKeys, initReq)
+ val initReq = impl.downloadInitialization(startDate, endDate)
+ val initResp = impl.postBTS(client, initReq, "Download init phase")
if (initResp.bankCode == EbicsReturnCode.EBICS_NO_DOWNLOAD_DATA_AVAILABLE) {
logger.debug("Download content is empty")
return@withContext
@@ -210,7 +209,7 @@ suspend fun ebicsDownload(
/** Send download receipt */
suspend fun receipt(success: Boolean) {
val xml = impl.downloadReceipt(tId, success)
- postBTS(client, cfg, bankKeys, xml).okOrFail("Download receipt phase")
+ impl.postBTS(client, xml, "Download receipt phase").okOrFail("Download receipt phase")
}
/** Throw if parent scope have been canceled */
suspend fun checkCancellation() {
@@ -227,7 +226,7 @@ suspend fun ebicsDownload(
for (x in 2 .. howManySegments) {
checkCancellation()
val transReq = impl.downloadTransfer(x, howManySegments, tId)
- val transResp = postBTS(client, cfg, bankKeys, transReq).okOrFail("Download transfer phase")
+ val transResp = impl.postBTS(client, transReq, "Download transfer phase").okOrFail("Download transfer phase")
val chunk = requireNotNull(transResp.payloadChunk) {
"Download transfer phase: missing encrypted chunk"
}
@@ -329,23 +328,23 @@ suspend fun doEbicsUpload(
cfg: EbicsSetupConfig,
clientKeys: ClientPrivateKeysFile,
bankKeys: BankPublicKeysFile,
- service: Ebics3Service,
+ order: EbicsOrder,
payload: ByteArray,
): String = withContext(NonCancellable) {
- val impl = Ebics3BTS(cfg, bankKeys, clientKeys)
+ val impl = EbicsBTS(cfg, bankKeys, clientKeys, order)
// TODO use a lambda and pass the order detail there for atomicity ?
val preparedPayload = prepareUploadPayload(cfg, clientKeys, bankKeys, payload)
// Init phase
- val initXml = impl.uploadInitialization(service, preparedPayload)
- val initResp = postBTS(client, cfg, bankKeys, initXml).okOrFail("Upload init phase")
+ val initXml = impl.uploadInitialization(preparedPayload)
+ val initResp = impl.postBTS(client, initXml, "Upload init phase").okOrFail("Upload init phase")
val tId = requireNotNull(initResp.transactionID) {
"Upload init phase: missing transaction ID"
}
// Transfer phase
val transferXml = impl.uploadTransfer(tId, preparedPayload)
- val transferResp = postBTS(client, cfg, bankKeys, transferXml).okOrFail("Upload transfer phase")
+ val transferResp = impl.postBTS(client, transferXml, "Upload transfer phase").okOrFail("Upload transfer phase")
val orderId = requireNotNull(transferResp.orderID) {
"Upload transfer phase: missing order ID"
}
@@ -374,45 +373,6 @@ class DataEncryptionInfo(
val bankPubDigest: ByteArray
)
-/**
- * Collects all the steps to prepare the submission of a pain.001
- * document to the bank, and finally send it. Indirectly throws
- * [EbicsSideException] or [EbicsUploadException]. The first means
- * that the bank sent an invalid response or signature, the second
- * that a proper EBICS or business error took place. The caller must
- * catch those exceptions and decide the retry policy.
- *
- * @param pain001xml pain.001 document in XML. The caller should
- * ensure its validity.
- * @param cfg configuration handle.
- * @param clientKeys client private keys.
- * @param bankkeys bank public keys.
- * @param httpClient HTTP client to connect to the bank.
- */
-suspend fun submitPain001(
- pain001xml: ByteArray,
- cfg: EbicsSetupConfig,
- clientKeys: ClientPrivateKeysFile,
- bankkeys: BankPublicKeysFile,
- httpClient: HttpClient
-): String {
- val service = Ebics3Service(
- name = "MCT",
- scope = "CH",
- messageName = "pain.001",
- messageVersion = "09",
- container = null
- )
- return doEbicsUpload(
- httpClient,
- cfg,
- clientKeys,
- bankkeys,
- service,
- pain001xml,
- )
-}
-
class EbicsResponse(
val technicalCode: EbicsReturnCode,
val bankCode: EbicsReturnCode,
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsDialect.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsDialect.kt
deleted file mode 100644
index 5d672de0..00000000
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsDialect.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * This file is part of LibEuFin.
- * Copyright (C) 2024 Taler Systems S.A.
-
- * LibEuFin is free software; you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation; either version 3, or
- * (at your option) any later version.
-
- * LibEuFin is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
- * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
- * Public License for more details.
-
- * You should have received a copy of the GNU Affero General Public
- * License along with LibEuFin; see the file COPYING. If not, see
- *
- */
-package tech.libeufin.nexus.ebics
-
-// We will support more dialect in the future
-
-data class Ebics3Service(
- val name: String,
- val scope: String,
- val messageName: String,
- val messageVersion: String,
- val container: String?
-)
-
-fun downloadDocService(doc: SupportedDocument): Pair {
- return when (doc) {
- SupportedDocument.PAIN_002 -> Pair("BTD", Ebics3Service("PSR", "CH", "pain.002", "10", "ZIP"))
- SupportedDocument.CAMT_052 -> Pair("BTD", Ebics3Service("STM", "CH", "camt.052", "08", "ZIP"))
- SupportedDocument.CAMT_053 -> Pair("BTD", Ebics3Service("EOP", "CH", "camt.053", "08", "ZIP"))
- SupportedDocument.CAMT_054 -> Pair("BTD", Ebics3Service("REP", "CH", "camt.054", "08", "ZIP"))
- SupportedDocument.PAIN_002_LOGS -> Pair("HAC", null)
- }
-}
-
-fun uploadPaymentService(): Ebics3Service {
- return Ebics3Service(
- name = "MCT",
- scope = "CH",
- messageName = "pain.001",
- messageVersion = "09",
- container = null
- )
-}
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsKeyMng.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsKeyMng.kt
new file mode 100644
index 00000000..1847beec
--- /dev/null
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsKeyMng.kt
@@ -0,0 +1,212 @@
+/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 2024 Taler Systems S.A.
+
+ * LibEuFin is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3, or
+ * (at your option) any later version.
+
+ * LibEuFin is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
+ * Public License for more details.
+
+ * You should have received a copy of the GNU Affero General Public
+ * License along with LibEuFin; see the file COPYING. If not, see
+ *
+ */
+
+package tech.libeufin.nexus.ebics
+
+import org.w3c.dom.Document
+import tech.libeufin.common.crypto.CryptoUtil
+import tech.libeufin.common.*
+import tech.libeufin.nexus.*
+import tech.libeufin.nexus.BankPublicKeysFile
+import tech.libeufin.nexus.ClientPrivateKeysFile
+import tech.libeufin.nexus.EbicsSetupConfig
+import java.io.InputStream
+import java.time.Instant
+import java.time.ZoneId
+import java.util.*
+import javax.xml.datatype.DatatypeFactory
+import java.security.interfaces.*
+
+/** EBICS protocol for key management */
+class Ebics3KeyMng(
+ private val cfg: EbicsSetupConfig,
+ private val clientKeys: ClientPrivateKeysFile
+) {
+ fun INI(): ByteArray {
+ val inner = XMLOrderData(cfg, "ns2:SignaturePubKeyOrderData", "http://www.ebics.org/S001") {
+ el("ns2:SignaturePubKeyInfo") {
+ RSAKeyXml(clientKeys.signature_private_key)
+ el("ns2:SignatureVersion", "A006")
+ }
+ }
+ val doc = XmlBuilder.toDom("ebicsUnsecuredRequest", "urn:org:ebics:H004") {
+ attr("http://www.w3.org/2000/xmlns/", "xmlns", "urn:org:ebics:H004")
+ attr("http://www.w3.org/2000/xmlns/", "xmlns:ds", "http://www.w3.org/2000/09/xmldsig#")
+ attr("Version", "H004")
+ attr("Revision", "1")
+ el("header") {
+ attr("authenticate", "true")
+ el("static") {
+ el("HostID", cfg.ebicsHostId)
+ el("PartnerID", cfg.ebicsPartnerId)
+ el("UserID", cfg.ebicsUserId)
+ el("OrderDetails") {
+ el("OrderType", "INI")
+ el("OrderAttribute", "DZNNN")
+ }
+ el("SecurityMedium", "0200")
+ }
+ el("mutable")
+ }
+ el("body/DataTransfer/OrderData", inner)
+ }
+ return XMLUtil.convertDomToBytes(doc)
+ }
+
+ fun HIA(): ByteArray {
+ val inner = XMLOrderData(cfg, "ns2:HIARequestOrderData", "urn:org:ebics:H004") {
+ el("ns2:AuthenticationPubKeyInfo") {
+ RSAKeyXml(clientKeys.authentication_private_key)
+ el("ns2:AuthenticationVersion", "X002")
+ }
+ el("ns2:EncryptionPubKeyInfo") {
+ RSAKeyXml(clientKeys.encryption_private_key)
+ el("ns2:EncryptionVersion", "E002")
+ }
+ }
+ val doc = XmlBuilder.toDom("ebicsUnsecuredRequest", "urn:org:ebics:H004") {
+ attr("http://www.w3.org/2000/xmlns/", "xmlns", "urn:org:ebics:H004")
+ attr("http://www.w3.org/2000/xmlns/", "xmlns:ds", "http://www.w3.org/2000/09/xmldsig#")
+ attr("Version", "H004")
+ attr("Revision", "1")
+ el("header") {
+ attr("authenticate", "true")
+ el("static") {
+ el("HostID", cfg.ebicsHostId)
+ el("PartnerID", cfg.ebicsPartnerId)
+ el("UserID", cfg.ebicsUserId)
+ el("OrderDetails") {
+ el("OrderType", "HIA")
+ el("OrderAttribute", "DZNNN")
+ }
+ el("SecurityMedium", "0200")
+ }
+ el("mutable")
+ }
+ el("body/DataTransfer/OrderData", inner)
+ }
+ return XMLUtil.convertDomToBytes(doc)
+ }
+
+ fun HPB(): ByteArray {
+ val nonce = getNonce(128)
+ val doc = XmlBuilder.toDom("ebicsNoPubKeyDigestsRequest", "urn:org:ebics:H004") {
+ attr("http://www.w3.org/2000/xmlns/", "xmlns", "urn:org:ebics:H004")
+ attr("http://www.w3.org/2000/xmlns/", "xmlns:ds", "http://www.w3.org/2000/09/xmldsig#")
+ attr("Version", "H004")
+ attr("Revision", "1")
+ el("header") {
+ attr("authenticate", "true")
+ el("static") {
+ el("HostID", cfg.ebicsHostId)
+ el("Nonce", nonce.encodeUpHex())
+ el("Timestamp", Instant.now().xmlDateTime())
+ el("PartnerID", cfg.ebicsPartnerId)
+ el("UserID", cfg.ebicsUserId)
+ el("OrderDetails") {
+ el("OrderType", "HPB")
+ el("OrderAttribute", "DZHNN")
+ }
+ el("SecurityMedium", "0000")
+ }
+ el("mutable")
+ }
+ el("AuthSignature")
+ el("body")
+ }
+ XMLUtil.signEbicsDocument(doc, clientKeys.authentication_private_key, "H004")
+ return XMLUtil.convertDomToBytes(doc)
+ }
+
+ /* ----- Helpers ----- */
+
+ private fun XmlBuilder.RSAKeyXml(key: RSAPrivateCrtKey) {
+ el("ns2:PubKeyValue") {
+ el("ds:RSAKeyValue") {
+ el("ds:Modulus", key.modulus.encodeBase64())
+ el("ds:Exponent", key.publicExponent.encodeBase64())
+ }
+ }
+ }
+
+ private fun XMLOrderData(cfg: EbicsSetupConfig, name: String, schema: String, build: XmlBuilder.() -> Unit): String {
+ return XmlBuilder.toBytes(name) {
+ attr("xmlns:ds", "http://www.w3.org/2000/09/xmldsig#")
+ attr("xmlns:ns2", schema)
+ build()
+ el("ns2:PartnerID", cfg.ebicsPartnerId)
+ el("ns2:UserID", cfg.ebicsUserId)
+ }.inputStream().deflate().encodeBase64()
+ }
+
+ companion object {
+ fun parseResponse(doc: Document, clientEncryptionKey: RSAPrivateCrtKey): EbicsResponse {
+ return XmlDestructor.fromDoc(doc, "ebicsKeyManagementResponse") {
+ lateinit var technicalCode: EbicsReturnCode
+ lateinit var bankCode: EbicsReturnCode
+ var payload: InputStream? = null
+ one("header") {
+ one("mutable") {
+ technicalCode = EbicsReturnCode.lookup(one("ReturnCode").text())
+ }
+ }
+ one("body") {
+ bankCode = EbicsReturnCode.lookup(one("ReturnCode").text())
+ payload = opt("DataTransfer") {
+ val descriptionInfo = one("DataEncryptionInfo") {
+ DataEncryptionInfo(
+ one("TransactionKey").text().decodeBase64(),
+ one("EncryptionPubKeyDigest").text().decodeBase64()
+ )
+ }
+ val chunk = one("OrderData").text().decodeBase64()
+ decryptAndDecompressPayload(
+ clientEncryptionKey,
+ descriptionInfo,
+ listOf(chunk)
+ )
+ }
+ }
+ EbicsResponse(
+ technicalCode = technicalCode,
+ bankCode,
+ content = payload
+ )
+ }
+ }
+
+ fun parseHpbOrder(data: InputStream): Pair {
+ return XmlDestructor.fromStream(data, "HPBResponseOrderData") {
+ val authPub = one("AuthenticationPubKeyInfo").one("PubKeyValue").one("RSAKeyValue") {
+ CryptoUtil.loadRsaPublicKeyFromComponents(
+ one("Modulus").text().decodeBase64(),
+ one("Exponent").text().decodeBase64(),
+ )
+ }
+ val encPub = one("EncryptionPubKeyInfo").one("PubKeyValue").one("RSAKeyValue") {
+ CryptoUtil.loadRsaPublicKeyFromComponents(
+ one("Modulus").text().decodeBase64(),
+ one("Exponent").text().decodeBase64(),
+ )
+ }
+ Pair(authPub, encPub)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsOrder.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsOrder.kt
new file mode 100644
index 00000000..3c73fff0
--- /dev/null
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsOrder.kt
@@ -0,0 +1,59 @@
+/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 2024 Taler Systems S.A.
+
+ * LibEuFin is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3, or
+ * (at your option) any later version.
+
+ * LibEuFin is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
+ * Public License for more details.
+
+ * You should have received a copy of the GNU Affero General Public
+ * License along with LibEuFin; see the file COPYING. If not, see
+ *
+ */
+package tech.libeufin.nexus.ebics
+
+// We will support more dialect in the future
+
+sealed class EbicsOrder(val schema: String) {
+ data class V2_5(
+ val type: String,
+ val attribute: String
+ ): EbicsOrder("H004")
+ data class V3(
+ val type: String,
+ val name: String? = null,
+ val scope: String? = null,
+ val messageName: String? = null,
+ val messageVersion: String? = null,
+ val container: String? = null,
+ ): EbicsOrder("H005")
+}
+
+fun downloadDocService(doc: SupportedDocument, ebics2: Boolean): EbicsOrder {
+ return if (ebics2) {
+ when (doc) {
+ SupportedDocument.PAIN_002 -> EbicsOrder.V2_5("Z01", "DZHNN")
+ SupportedDocument.CAMT_052 -> EbicsOrder.V2_5("Z52", "DZHNN")
+ SupportedDocument.CAMT_053 -> EbicsOrder.V2_5("Z53", "DZHNN")
+ SupportedDocument.CAMT_054 -> EbicsOrder.V2_5("Z54", "DZHNN")
+ SupportedDocument.PAIN_002_LOGS -> EbicsOrder.V2_5("HAC", "DZHNN")
+ }
+ } else {
+ when (doc) {
+ SupportedDocument.PAIN_002 -> EbicsOrder.V3("BTD", "PSR", "CH", "pain.002", "10", "ZIP")
+ SupportedDocument.CAMT_052 -> EbicsOrder.V3("BTD", "STM", "CH", "camt.052", "08", "ZIP")
+ SupportedDocument.CAMT_053 -> EbicsOrder.V3("BTD", "EOP", "CH", "camt.053", "08", "ZIP")
+ SupportedDocument.CAMT_054 -> EbicsOrder.V3("BTD", "REP", "CH", "camt.054", "08", "ZIP")
+ SupportedDocument.PAIN_002_LOGS -> EbicsOrder.V3("HAC")
+ }
+ }
+}
+
+fun uploadPaymentService(): EbicsOrder =
+ EbicsOrder.V3("BTU", "MCT", "CH", "pain.001", "09")
--
cgit v1.2.3