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